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.

CellFormatPart.java 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.ss.format;
  16. import org.apache.logging.log4j.LogManager;
  17. import org.apache.logging.log4j.Logger;
  18. import org.apache.poi.hssf.util.HSSFColor;
  19. import org.apache.poi.util.CodepointsUtil;
  20. import org.apache.poi.util.LocaleUtil;
  21. import javax.swing.*;
  22. import java.awt.*;
  23. import java.util.*;
  24. import java.util.List;
  25. import java.util.regex.Matcher;
  26. import java.util.regex.Pattern;
  27. import static org.apache.poi.ss.format.CellFormatter.quote;
  28. /**
  29. * Objects of this class represent a single part of a cell format expression.
  30. * Each cell can have up to four of these for positive, zero, negative, and text
  31. * values.
  32. * <p>
  33. * Each format part can contain a color, a condition, and will always contain a
  34. * format specification. For example {@code "[Red][>=10]#"} has a color
  35. * ({@code [Red]}), a condition ({@code >=10}) and a format specification
  36. * ({@code #}).
  37. * <p>
  38. * This class also contains patterns for matching the subparts of format
  39. * specification. These are used internally, but are made public in case other
  40. * code has use for them.
  41. */
  42. @SuppressWarnings("RegExpRepeatedSpace")
  43. public class CellFormatPart {
  44. private static final Logger LOG = LogManager.getLogger(CellFormatPart.class);
  45. static final Map<String, Color> NAMED_COLORS;
  46. private final Color color;
  47. private final CellFormatCondition condition;
  48. private final CellFormatter format;
  49. private final CellFormatType type;
  50. static {
  51. NAMED_COLORS = new TreeMap<>(
  52. String.CASE_INSENSITIVE_ORDER);
  53. for (HSSFColor.HSSFColorPredefined color : HSSFColor.HSSFColorPredefined.values()) {
  54. String name = color.name();
  55. short[] rgb = color.getTriplet();
  56. Color c = new Color(rgb[0], rgb[1], rgb[2]);
  57. NAMED_COLORS.put(name, c);
  58. if (name.indexOf('_') > 0)
  59. NAMED_COLORS.put(name.replace('_', ' '), c);
  60. if (name.indexOf("_PERCENT") > 0)
  61. NAMED_COLORS.put(name.replace("_PERCENT", "%").replace('_',
  62. ' '), c);
  63. }
  64. }
  65. /** Pattern for the color part of a cell format part. */
  66. public static final Pattern COLOR_PAT;
  67. /** Pattern for the condition part of a cell format part. */
  68. public static final Pattern CONDITION_PAT;
  69. /** Pattern for the format specification part of a cell format part. */
  70. public static final Pattern SPECIFICATION_PAT;
  71. /** Pattern for the currency symbol part of a cell format part */
  72. public static final Pattern CURRENCY_PAT;
  73. /** Pattern for an entire cell single part. */
  74. public static final Pattern FORMAT_PAT;
  75. /** Within {@link #FORMAT_PAT}, the group number for the matched color. */
  76. public static final int COLOR_GROUP;
  77. /**
  78. * Within {@link #FORMAT_PAT}, the group number for the operator in the
  79. * condition.
  80. */
  81. public static final int CONDITION_OPERATOR_GROUP;
  82. /**
  83. * Within {@link #FORMAT_PAT}, the group number for the value in the
  84. * condition.
  85. */
  86. public static final int CONDITION_VALUE_GROUP;
  87. /**
  88. * Within {@link #FORMAT_PAT}, the group number for the format
  89. * specification.
  90. */
  91. public static final int SPECIFICATION_GROUP;
  92. static {
  93. // A condition specification
  94. String condition = "([<>=]=?|!=|<>) # The operator\n" +
  95. " \\s*(-?([0-9]+(?:\\.[0-9]*)?)|(\\.[0-9]*))\\s* # The constant to test against\n";
  96. // A currency symbol / string, in a specific locale
  97. String currency = "(\\[\\$.{0,3}(-[0-9a-f]{3,4})?])";
  98. String color =
  99. "\\[(black|blue|cyan|green|magenta|red|white|yellow|color [0-9]+)]";
  100. // A number specification
  101. // Note: careful that in something like ##, that the trailing comma is not caught up in the integer part
  102. // A part of a specification
  103. //noinspection RegExpRedundantEscape
  104. String part = "\\\\. # Quoted single character\n" +
  105. "|\"([^\\\\\"]|\\\\.)*\" # Quoted string of characters (handles escaped quotes like \\\") \n" +
  106. "|"+currency+" # Currency symbol in a given locale\n" +
  107. "|_. # Space as wide as a given character\n" +
  108. "|\\*. # Repeating fill character\n" +
  109. "|@ # Text: cell text\n" +
  110. "|([0?\\#][0?\\#,]*) # Number: digit + other digits and commas\n" +
  111. "|e[-+] # Number: Scientific: Exponent\n" +
  112. "|m{1,5} # Date: month or minute spec\n" +
  113. "|d{1,4} # Date: day/date spec\n" +
  114. "|y{2,4} # Date: year spec\n" +
  115. "|h{1,2} # Date: hour spec\n" +
  116. "|s{1,2} # Date: second spec\n" +
  117. "|am?/pm? # Date: am/pm spec\n" +
  118. "|\\[h{1,2}] # Elapsed time: hour spec\n" +
  119. "|\\[m{1,2}] # Elapsed time: minute spec\n" +
  120. "|\\[s{1,2}] # Elapsed time: second spec\n" +
  121. "|[^;] # A character\n" + "";
  122. String format = "(?:" + color + ")? # Text color\n" +
  123. "(?:\\[" + condition + "])? # Condition\n" +
  124. // see https://msdn.microsoft.com/en-ca/goglobal/bb964664.aspx and https://bz.apache.org/ooo/show_bug.cgi?id=70003
  125. // we ignore these for now though
  126. "(?:\\[\\$-[0-9a-fA-F]+])? # Optional locale id, ignored currently\n" +
  127. "((?:" + part + ")+) # Format spec\n";
  128. int flags = Pattern.COMMENTS | Pattern.CASE_INSENSITIVE;
  129. COLOR_PAT = Pattern.compile(color, flags);
  130. CONDITION_PAT = Pattern.compile(condition, flags);
  131. SPECIFICATION_PAT = Pattern.compile(part, flags);
  132. CURRENCY_PAT = Pattern.compile(currency, flags);
  133. FORMAT_PAT = Pattern.compile(format, flags);
  134. // Calculate the group numbers of important groups. (They shift around
  135. // when the pattern is changed; this way we figure out the numbers by
  136. // experimentation.)
  137. COLOR_GROUP = findGroup(FORMAT_PAT, "[Blue]@", "Blue");
  138. CONDITION_OPERATOR_GROUP = findGroup(FORMAT_PAT, "[>=1]@", ">=");
  139. CONDITION_VALUE_GROUP = findGroup(FORMAT_PAT, "[>=1]@", "1");
  140. SPECIFICATION_GROUP = findGroup(FORMAT_PAT, "[Blue][>1]\\a ?", "\\a ?");
  141. }
  142. interface PartHandler {
  143. String handlePart(Matcher m, String part, CellFormatType type,
  144. StringBuffer desc);
  145. }
  146. /**
  147. * Create an object to represent a format part.
  148. *
  149. * @param desc The string to parse.
  150. */
  151. public CellFormatPart(String desc) {
  152. this(LocaleUtil.getUserLocale(), desc);
  153. }
  154. /**
  155. * Create an object to represent a format part.
  156. *
  157. * @param locale The locale to use.
  158. * @param desc The string to parse.
  159. */
  160. public CellFormatPart(Locale locale, String desc) {
  161. Matcher m = FORMAT_PAT.matcher(desc);
  162. if (!m.matches()) {
  163. throw new IllegalArgumentException("Unrecognized format: " + quote(
  164. desc));
  165. }
  166. color = getColor(m);
  167. condition = getCondition(m);
  168. type = getCellFormatType(m);
  169. format = getFormatter(locale, m);
  170. }
  171. /**
  172. * Returns {@code true} if this format part applies to the given value. If
  173. * the value is a number and this is part has a condition, returns
  174. * {@code true} only if the number passes the condition. Otherwise, this
  175. * always return {@code true}.
  176. *
  177. * @param valueObject The value to evaluate.
  178. *
  179. * @return {@code true} if this format part applies to the given value.
  180. */
  181. public boolean applies(Object valueObject) {
  182. if (condition == null || !(valueObject instanceof Number)) {
  183. if (valueObject == null)
  184. throw new NullPointerException("valueObject");
  185. return true;
  186. } else {
  187. Number num = (Number) valueObject;
  188. return condition.pass(num.doubleValue());
  189. }
  190. }
  191. /**
  192. * Returns the number of the first group that is the same as the marker
  193. * string. Starts from group 1.
  194. *
  195. * @param pat The pattern to use.
  196. * @param str The string to match against the pattern.
  197. * @param marker The marker value to find the group of.
  198. *
  199. * @return The matching group number.
  200. *
  201. * @throws IllegalArgumentException No group matches the marker.
  202. */
  203. private static int findGroup(Pattern pat, String str, String marker) {
  204. Matcher m = pat.matcher(str);
  205. if (!m.find())
  206. throw new IllegalArgumentException(
  207. "Pattern \"" + pat.pattern() + "\" doesn't match \"" + str +
  208. "\"");
  209. for (int i = 1; i <= m.groupCount(); i++) {
  210. String grp = m.group(i);
  211. if (grp != null && grp.equals(marker))
  212. return i;
  213. }
  214. throw new IllegalArgumentException(
  215. "\"" + marker + "\" not found in \"" + pat.pattern() + "\"");
  216. }
  217. /**
  218. * Returns the color specification from the matcher, or {@code null} if
  219. * there is none.
  220. *
  221. * @param m The matcher for the format part.
  222. *
  223. * @return The color specification or {@code null}.
  224. */
  225. private static Color getColor(Matcher m) {
  226. String cdesc = m.group(COLOR_GROUP);
  227. if (cdesc == null || cdesc.length() == 0)
  228. return null;
  229. Color c = NAMED_COLORS.get(cdesc);
  230. if (c == null) {
  231. LOG.warn("Unknown color: {}", quote(cdesc));
  232. }
  233. return c;
  234. }
  235. /**
  236. * Returns the condition specification from the matcher, or {@code null} if
  237. * there is none.
  238. *
  239. * @param m The matcher for the format part.
  240. *
  241. * @return The condition specification or {@code null}.
  242. */
  243. private CellFormatCondition getCondition(Matcher m) {
  244. String mdesc = m.group(CONDITION_OPERATOR_GROUP);
  245. if (mdesc == null || mdesc.length() == 0)
  246. return null;
  247. return CellFormatCondition.getInstance(m.group(
  248. CONDITION_OPERATOR_GROUP), m.group(CONDITION_VALUE_GROUP));
  249. }
  250. /**
  251. * Returns the CellFormatType object implied by the format specification for
  252. * the format part.
  253. *
  254. * @param matcher The matcher for the format part.
  255. *
  256. * @return The CellFormatType.
  257. */
  258. private CellFormatType getCellFormatType(Matcher matcher) {
  259. String fdesc = matcher.group(SPECIFICATION_GROUP);
  260. return formatType(fdesc);
  261. }
  262. /**
  263. * Returns the formatter object implied by the format specification for the
  264. * format part.
  265. *
  266. * @param matcher The matcher for the format part.
  267. *
  268. * @return The formatter.
  269. */
  270. private CellFormatter getFormatter(Locale locale, Matcher matcher) {
  271. String fdesc = matcher.group(SPECIFICATION_GROUP);
  272. // For now, we don't support localised currencies, so simplify if there
  273. Matcher currencyM = CURRENCY_PAT.matcher(fdesc);
  274. if (currencyM.find()) {
  275. String currencyPart = currencyM.group(1);
  276. String currencyRepl;
  277. if (currencyPart.startsWith("[$-")) {
  278. // Default $ in a different locale
  279. currencyRepl = "$";
  280. } else if (!currencyPart.contains("-")) {
  281. // Accounting formats such as USD [$USD]
  282. currencyRepl = currencyPart.substring(2, currencyPart.indexOf("]"));
  283. } else {
  284. currencyRepl = currencyPart.substring(2, currencyPart.lastIndexOf('-'));
  285. }
  286. fdesc = fdesc.replace(currencyPart, currencyRepl);
  287. }
  288. // Build a formatter for this simplified string
  289. return type.formatter(locale, fdesc);
  290. }
  291. /**
  292. * Returns the type of format.
  293. *
  294. * @param fdesc The format specification
  295. *
  296. * @return The type of format.
  297. */
  298. private CellFormatType formatType(String fdesc) {
  299. fdesc = fdesc.trim();
  300. if (fdesc.isEmpty() || fdesc.equalsIgnoreCase("General"))
  301. return CellFormatType.GENERAL;
  302. Matcher m = SPECIFICATION_PAT.matcher(fdesc);
  303. boolean couldBeDate = false;
  304. boolean seenZero = false;
  305. while (m.find()) {
  306. String repl = m.group(0);
  307. Iterator<String> codePoints = CodepointsUtil.iteratorFor(repl);
  308. if (codePoints.hasNext()) {
  309. String c1 = codePoints.next();
  310. switch (c1) {
  311. case "@":
  312. return CellFormatType.TEXT;
  313. case "d":
  314. case "D":
  315. case "y":
  316. case "Y":
  317. return CellFormatType.DATE;
  318. case "h":
  319. case "H":
  320. case "m":
  321. case "M":
  322. case "s":
  323. case "S":
  324. // These can be part of date, or elapsed
  325. couldBeDate = true;
  326. break;
  327. case "0":
  328. // This can be part of date, elapsed, or number
  329. seenZero = true;
  330. break;
  331. case "[":
  332. String c2 = null;
  333. if (codePoints.hasNext())
  334. c2 = codePoints.next().toLowerCase(Locale.ROOT);
  335. if ("h".equals(c2) || "m".equals(c2) || "s".equals(c2)) {
  336. return CellFormatType.ELAPSED;
  337. }
  338. if ("$".equals(c2)) {
  339. // Localised currency
  340. return CellFormatType.NUMBER;
  341. }
  342. // Something else inside [] which isn't supported!
  343. throw new IllegalArgumentException("Unsupported [] format block '" +
  344. repl + "' in '" + fdesc + "' with c2: " + c2);
  345. case "#":
  346. case "?":
  347. return CellFormatType.NUMBER;
  348. }
  349. }
  350. }
  351. // Nothing definitive was found, so we figure out it deductively
  352. if (couldBeDate)
  353. return CellFormatType.DATE;
  354. if (seenZero)
  355. return CellFormatType.NUMBER;
  356. return CellFormatType.TEXT;
  357. }
  358. /**
  359. * Returns a version of the original string that has any special characters
  360. * quoted (or escaped) as appropriate for the cell format type. The format
  361. * type object is queried to see what is special.
  362. *
  363. * @param repl The original string.
  364. * @param type The format type representation object.
  365. *
  366. * @return A version of the string with any special characters replaced.
  367. *
  368. * @see CellFormatType#isSpecial(char)
  369. */
  370. static String quoteSpecial(String repl, CellFormatType type) {
  371. StringBuilder sb = new StringBuilder();
  372. PrimitiveIterator.OfInt codePoints = CodepointsUtil.primitiveIterator(repl);
  373. int codepoint;
  374. while (codePoints.hasNext()) {
  375. codepoint = codePoints.nextInt();
  376. if (codepoint == '\'' && type.isSpecial('\'')) {
  377. sb.append('\u0000');
  378. continue;
  379. }
  380. char[] chars = Character.toChars(codepoint);
  381. boolean special = type.isSpecial(chars[0]);
  382. if (special)
  383. sb.append('\'');
  384. sb.append(chars);
  385. if (special)
  386. sb.append('\'');
  387. }
  388. return sb.toString();
  389. }
  390. /**
  391. * Apply this format part to the given value. This returns a {@link
  392. * CellFormatResult} object with the results.
  393. *
  394. * @param value The value to apply this format part to.
  395. *
  396. * @return A {@link CellFormatResult} object containing the results of
  397. * applying the format to the value.
  398. */
  399. public CellFormatResult apply(Object value) {
  400. boolean applies = applies(value);
  401. String text;
  402. Color textColor;
  403. if (applies) {
  404. text = format.format(value);
  405. textColor = color;
  406. } else {
  407. text = format.simpleFormat(value);
  408. textColor = null;
  409. }
  410. return new CellFormatResult(applies, text, textColor);
  411. }
  412. /**
  413. * Apply this format part to the given value, applying the result to the
  414. * given label.
  415. *
  416. * @param label The label
  417. * @param value The value to apply this format part to.
  418. *
  419. * @return {@code true} if the
  420. */
  421. public CellFormatResult apply(JLabel label, Object value) {
  422. CellFormatResult result = apply(value);
  423. label.setText(result.text);
  424. if (result.textColor != null) {
  425. label.setForeground(result.textColor);
  426. }
  427. return result;
  428. }
  429. /**
  430. * Returns the CellFormatType object implied by the format specification for
  431. * the format part.
  432. *
  433. * @return The CellFormatType.
  434. */
  435. CellFormatType getCellFormatType() {
  436. return type;
  437. }
  438. /**
  439. * Returns {@code true} if this format part has a condition.
  440. *
  441. * @return {@code true} if this format part has a condition.
  442. */
  443. boolean hasCondition() {
  444. return condition != null;
  445. }
  446. public static StringBuffer parseFormat(String fdesc, CellFormatType type,
  447. PartHandler partHandler) {
  448. // Quoting is very awkward. In the Java classes, quoting is done
  449. // between ' chars, with '' meaning a single ' char. The problem is that
  450. // in Excel, it is legal to have two adjacent escaped strings. For
  451. // example, consider the Excel format "\a\b#". The naive (and easy)
  452. // translation into Java DecimalFormat is "'a''b'#". For the number 17,
  453. // in Excel you would get "ab17", but in Java it would be "a'b17" -- the
  454. // '' is in the middle of the quoted string in Java. So the trick we
  455. // use is this: When we encounter a ' char in the Excel format, we
  456. // output a \u0000 char into the string. Now we know that any '' in the
  457. // output is the result of two adjacent escaped strings. So after the
  458. // main loop, we have to do two passes: One to eliminate any ''
  459. // sequences, to make "'a''b'" become "'ab'", and another to replace any
  460. // \u0000 with '' to mean a quote char. Oy.
  461. //
  462. // For formats that don't use "'" we don't do any of this
  463. Matcher m = SPECIFICATION_PAT.matcher(fdesc);
  464. StringBuffer fmt = new StringBuffer();
  465. while (m.find()) {
  466. String part = group(m, 0);
  467. if (part.length() > 0) {
  468. String repl = partHandler.handlePart(m, part, type, fmt);
  469. if (repl == null) {
  470. switch (part.charAt(0)) {
  471. case '\"':
  472. repl = quoteSpecial(part.substring(1,
  473. part.length() - 1), type);
  474. break;
  475. case '\\':
  476. repl = quoteSpecial(part.substring(1), type);
  477. break;
  478. case '_':
  479. repl = " ";
  480. break;
  481. case '*': //!! We don't do this for real, we just put in 3 of them
  482. repl = expandChar(part);
  483. break;
  484. default:
  485. repl = part;
  486. break;
  487. }
  488. }
  489. m.appendReplacement(fmt, Matcher.quoteReplacement(repl));
  490. }
  491. }
  492. m.appendTail(fmt);
  493. if (type.isSpecial('\'')) {
  494. // Now the next pass for quoted characters: Remove '' chars, making "'a''b'" into "'ab'"
  495. int pos = 0;
  496. while ((pos = fmt.indexOf("''", pos)) >= 0) {
  497. fmt.delete(pos, pos + 2);
  498. if (partHandler instanceof CellDateFormatter.DatePartHandler) {
  499. CellDateFormatter.DatePartHandler datePartHandler = (CellDateFormatter.DatePartHandler) partHandler;
  500. datePartHandler.updatePositions(pos, -2);
  501. }
  502. }
  503. // Now the final pass for quoted chars: Replace any \u0000 with ''
  504. pos = 0;
  505. while ((pos = fmt.indexOf("\u0000", pos)) >= 0) {
  506. fmt.replace(pos, pos + 1, "''");
  507. if (partHandler instanceof CellDateFormatter.DatePartHandler) {
  508. CellDateFormatter.DatePartHandler datePartHandler = (CellDateFormatter.DatePartHandler) partHandler;
  509. datePartHandler.updatePositions(pos, 1);
  510. }
  511. }
  512. }
  513. return fmt;
  514. }
  515. /**
  516. * Expands a character. This is only partly done, because we don't have the
  517. * correct info. In Excel, this would be expanded to fill the rest of the
  518. * cell, but we don't know, in general, what the "rest of the cell" is.
  519. *
  520. * @param part The character to be repeated is the second character in this
  521. * string.
  522. *
  523. * @return The character repeated three times.
  524. */
  525. static String expandChar(String part) {
  526. PrimitiveIterator.OfInt iterator = CodepointsUtil.primitiveIterator(part);
  527. Integer c0 = iterator.hasNext() ? iterator.next() : null;
  528. Integer c1 = iterator.hasNext() ? iterator.next() : null;
  529. if (c0 == null || c1 == null)
  530. throw new IllegalArgumentException("Expected part string to have at least 2 chars");
  531. char[] ch = Character.toChars(c1);
  532. StringBuilder sb = new StringBuilder(ch.length * 3);
  533. sb.append(ch);
  534. sb.append(ch);
  535. sb.append(ch);
  536. return sb.toString();
  537. }
  538. /**
  539. * Returns the string from the group, or {@code ""} if the group is
  540. * {@code null}.
  541. *
  542. * @param m The matcher.
  543. * @param g The group number.
  544. *
  545. * @return The group or {@code ""}.
  546. */
  547. public static String group(Matcher m, int g) {
  548. String str = m.group(g);
  549. return (str == null ? "" : str);
  550. }
  551. public String toString() {
  552. return format.format;
  553. }
  554. }