123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481 |
- /* ====================================================================
- Licensed to the Apache Software Foundation (ASF) under one or more
- contributor license agreements. See the NOTICE file distributed with
- this work for additional information regarding copyright ownership.
- The ASF licenses this file to You under the Apache License, Version 2.0
- (the "License"); you may not use this file except in compliance with
- the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- ==================================================================== */
-
- package org.apache.poi.ss.format;
-
- import java.util.ArrayList;
- import java.util.Date;
- import java.util.List;
- import java.util.Locale;
- import java.util.Map;
- import java.util.WeakHashMap;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
-
- import javax.swing.JLabel;
-
- import org.apache.logging.log4j.Level;
- import org.apache.logging.log4j.LogManager;
- import org.apache.logging.log4j.Logger;
- import org.apache.poi.ss.usermodel.Cell;
- import org.apache.poi.ss.usermodel.CellType;
- import org.apache.poi.ss.usermodel.ConditionalFormatting;
- import org.apache.poi.ss.usermodel.ConditionalFormattingRule;
- import org.apache.poi.ss.usermodel.DataFormatter;
- import org.apache.poi.ss.usermodel.DateUtil;
- import org.apache.poi.ss.util.DateFormatConverter;
- import org.apache.poi.util.LocaleUtil;
-
- /**
- * Format a value according to the standard Excel behavior. This "standard" is
- * not explicitly documented by Microsoft, so the behavior is determined by
- * experimentation; see the tests.
- * <p>
- * An Excel format has up to four parts, separated by semicolons. Each part
- * specifies what to do with particular kinds of values, depending on the number
- * of parts given:
- * <dl>
- * <dt>One part (example: {@code [Green]#.##})</dt>
- * <dd>If the value is a number, display according to this one part (example: green text,
- * with up to two decimal points). If the value is text, display it as is.</dd>
- *
- * <dt>Two parts (example: {@code [Green]#.##;[Red]#.##})</dt>
- * <dd>If the value is a positive number or zero, display according to the first part (example: green
- * text, with up to two decimal points); if it is a negative number, display
- * according to the second part (example: red text, with up to two decimal
- * points). If the value is text, display it as is.</dd>
- *
- * <dt>Three parts (example: {@code [Green]#.##;[Black]#.##;[Red]#.##})</dt>
- * <dd>If the value is a positive
- * number, display according to the first part (example: green text, with up to
- * two decimal points); if it is zero, display according to the second part
- * (example: black text, with up to two decimal points); if it is a negative
- * number, display according to the third part (example: red text, with up to
- * two decimal points). If the value is text, display it as is.</dd>
- *
- * <dt>Four parts (example: {@code [Green]#.##;[Black]#.##;[Red]#.##;[@]})</dt>
- * <dd>If the value is a positive number, display according to the first part (example: green text,
- * with up to two decimal points); if it is zero, display according to the
- * second part (example: black text, with up to two decimal points); if it is a
- * negative number, display according to the third part (example: red text, with
- * up to two decimal points). If the value is text, display according to the
- * fourth part (example: text in the cell's usual color, with the text value
- * surround by brackets).</dd>
- * </dl>
- * <p>
- * A given format part may specify a given Locale, by including something
- * like {@code [$$-409]} or {@code [$£-809]} or {@code [$-40C]}. These
- * are (currently) largely ignored. You can use {@link DateFormatConverter}
- * to look these up into Java Locales if desired.
- * <p>
- * In addition to these, there is a general format that is used when no format
- * is specified.
- *
- * TODO Merge this with {@link DataFormatter} so we only have one set of
- * code for formatting numbers.
- * TODO Re-use parts of this logic with {@link ConditionalFormatting} /
- * {@link ConditionalFormattingRule} for reporting stylings which do/don't apply
- * TODO Support the full set of modifiers, including alternate calendars and
- * native character numbers, as documented at https://help.libreoffice.org/Common/Number_Format_Codes
- */
- public class CellFormat {
- /** The logger to use in the formatting code. */
- private static final Logger LOG = LogManager.getLogger(CellFormat.class);
-
- private static final Pattern ONE_PART = Pattern.compile(
- CellFormatPart.FORMAT_PAT.pattern() + "(;|$)",
- Pattern.COMMENTS | Pattern.CASE_INSENSITIVE);
-
- /*
- * Cells that cannot be formatted, e.g. cells that have a date or time
- * format and have an invalid date or time value, are displayed as 255
- * pound signs ("#").
- */
- private static final String INVALID_VALUE_FOR_FORMAT =
- "###################################################" +
- "###################################################" +
- "###################################################" +
- "###################################################" +
- "###################################################";
-
- private static final String QUOTE = "\"";
-
- private final Locale locale;
- private final String format;
- private final CellFormatPart posNumFmt;
- private final CellFormatPart zeroNumFmt;
- private final CellFormatPart negNumFmt;
- private final CellFormatPart textFmt;
- private final int formatPartCount;
-
- private static CellFormat createGeneralFormat(final Locale locale) {
- return new CellFormat(locale, "General") {
- @Override
- public CellFormatResult apply(Object value) {
- String text = (new CellGeneralFormatter(locale)).format(value);
- return new CellFormatResult(true, text, null);
- }
- };
- }
-
- /** Maps a format string to its parsed version for efficiencies sake. */
- private static final Map<Locale, Map<String, CellFormat>> formatCache =
- new WeakHashMap<>();
-
- /**
- * Returns a CellFormat that applies the given format. Two calls
- * with the same format may or may not return the same object.
- *
- * @param format The format.
- *
- * @return A CellFormat that applies the given format.
- */
- public static CellFormat getInstance(String format) {
- return getInstance(LocaleUtil.getUserLocale(), format);
- }
-
- /**
- * Returns a CellFormat that applies the given format. Two calls
- * with the same format may or may not return the same object.
- *
- * @param locale The locale.
- * @param format The format.
- *
- * @return A CellFormat that applies the given format.
- */
- public static synchronized CellFormat getInstance(Locale locale, String format) {
- Map<String, CellFormat> formatMap = formatCache.computeIfAbsent(locale, k -> new WeakHashMap<>());
- CellFormat fmt = formatMap.get(format);
- if (fmt == null) {
- if (format.equals("General") || format.equals("@"))
- fmt = createGeneralFormat(locale);
- else
- fmt = new CellFormat(locale, format);
- formatMap.put(format, fmt);
- }
- return fmt;
- }
-
- /**
- * Creates a new object.
- *
- * @param format The format.
- */
- private CellFormat(Locale locale, String format) {
- this.locale = locale;
- this.format = format;
- CellFormatPart defaultTextFormat = new CellFormatPart(locale, "@");
- Matcher m = ONE_PART.matcher(format);
- List<CellFormatPart> parts = new ArrayList<>();
-
- while (m.find()) {
- try {
- String valueDesc = m.group();
-
- // Strip out the semicolon if it's there
- if (valueDesc.endsWith(";"))
- valueDesc = valueDesc.substring(0, valueDesc.length() - 1);
-
- parts.add(new CellFormatPart(locale, valueDesc));
- } catch (RuntimeException e) {
- LOG.warn("Invalid format: {}", CellFormatter.quote(m.group()), e);
- parts.add(null);
- }
- }
-
- formatPartCount = parts.size();
-
- switch (formatPartCount) {
- case 1:
- posNumFmt = parts.get(0);
- negNumFmt = null;
- zeroNumFmt = null;
- textFmt = defaultTextFormat;
- break;
- case 2:
- posNumFmt = parts.get(0);
- negNumFmt = parts.get(1);
- zeroNumFmt = null;
- textFmt = defaultTextFormat;
- break;
- case 3:
- posNumFmt = parts.get(0);
- negNumFmt = parts.get(1);
- zeroNumFmt = parts.get(2);
- textFmt = defaultTextFormat;
- break;
- case 4:
- default:
- posNumFmt = parts.get(0);
- negNumFmt = parts.get(1);
- zeroNumFmt = parts.get(2);
- textFmt = parts.get(3);
- break;
- }
- }
-
- /**
- * Returns the result of applying the format to the given value. If the
- * value is a number (a type of {@link Number} object), the correct number
- * format type is chosen; otherwise it is considered a text object.
- *
- * @param value The value
- *
- * @return The result, in a {@link CellFormatResult}.
- */
- public CellFormatResult apply(Object value) {
- if (value instanceof Number) {
- Number num = (Number) value;
- double val = num.doubleValue();
- if (val < 0 &&
- ((formatPartCount == 2
- && !posNumFmt.hasCondition() && !negNumFmt.hasCondition())
- || (formatPartCount == 3 && !negNumFmt.hasCondition())
- || (formatPartCount == 4 && !negNumFmt.hasCondition()))) {
- // The negative number format has the negative formatting required,
- // e.g. minus sign or brackets, so pass a positive value so that
- // the default leading minus sign is not also output
- return negNumFmt.apply(-val);
- } else {
- return getApplicableFormatPart(val).apply(val);
- }
- } else if (value instanceof java.util.Date) {
- // Don't know (and can't get) the workbook date windowing (1900 or 1904)
- // so assume 1900 date windowing
- double numericValue = DateUtil.getExcelDate((Date) value);
- if (DateUtil.isValidExcelDate(numericValue)) {
- return getApplicableFormatPart(numericValue).apply(value);
- } else {
- throw new IllegalArgumentException("value " + numericValue + " of date " + value + " is not a valid Excel date");
- }
- } else {
- return textFmt.apply(value);
- }
- }
-
- /**
- * Returns the result of applying the format to the given date.
- *
- * @param date The date.
- * @param numericValue The numeric value for the date.
- *
- * @return The result, in a {@link CellFormatResult}.
- */
- private CellFormatResult apply(Date date, double numericValue) {
- return getApplicableFormatPart(numericValue).apply(date);
- }
-
- /**
- * Fetches the appropriate value from the cell, and returns the result of
- * applying it to the appropriate format. For formula cells, the computed
- * value is what is used.
- *
- * @param c The cell.
- *
- * @return The result, in a {@link CellFormatResult}.
- */
- public CellFormatResult apply(Cell c) {
- switch (ultimateType(c)) {
- case BLANK:
- return apply("");
- case BOOLEAN:
- return apply(c.getBooleanCellValue());
- case NUMERIC:
- double value = c.getNumericCellValue();
- if (getApplicableFormatPart(value).getCellFormatType() == CellFormatType.DATE) {
- if (DateUtil.isValidExcelDate(value)) {
- return apply(c.getDateCellValue(), value);
- } else {
- return apply(INVALID_VALUE_FOR_FORMAT);
- }
- } else {
- return apply(value);
- }
- case STRING:
- return apply(c.getStringCellValue());
- default:
- return apply("?");
- }
- }
-
- /**
- * Uses the result of applying this format to the value, setting the text
- * and color of a label before returning the result.
- *
- * @param label The label to apply to.
- * @param value The value to process.
- *
- * @return The result, in a {@link CellFormatResult}.
- */
- public CellFormatResult apply(JLabel label, Object value) {
- CellFormatResult result = apply(value);
- label.setText(result.text);
- if (result.textColor != null) {
- label.setForeground(result.textColor);
- }
- return result;
- }
-
- /**
- * Uses the result of applying this format to the given date, setting the text
- * and color of a label before returning the result.
- *
- * @param label The label to apply to.
- * @param date The date.
- * @param numericValue The numeric value for the date.
- *
- * @return The result, in a {@link CellFormatResult}.
- */
- private CellFormatResult apply(JLabel label, Date date, double numericValue) {
- CellFormatResult result = apply(date, numericValue);
- label.setText(result.text);
- if (result.textColor != null) {
- label.setForeground(result.textColor);
- }
- return result;
- }
-
- /**
- * Fetches the appropriate value from the cell, and uses the result, setting
- * the text and color of a label before returning the result.
- *
- * @param label The label to apply to.
- * @param c The cell.
- *
- * @return The result, in a {@link CellFormatResult}.
- */
- public CellFormatResult apply(JLabel label, Cell c) {
- switch (ultimateType(c)) {
- case BLANK:
- return apply(label, "");
- case BOOLEAN:
- return apply(label, c.getBooleanCellValue());
- case NUMERIC:
- double value = c.getNumericCellValue();
- if (getApplicableFormatPart(value).getCellFormatType() == CellFormatType.DATE) {
- if (DateUtil.isValidExcelDate(value)) {
- return apply(label, c.getDateCellValue(), value);
- } else {
- return apply(label, INVALID_VALUE_FOR_FORMAT);
- }
- } else {
- return apply(label, value);
- }
- case STRING:
- return apply(label, c.getStringCellValue());
- default:
- return apply(label, "?");
- }
- }
-
- /**
- * Returns the {@link CellFormatPart} that applies to the value. Result
- * depends on how many parts the cell format has, the cell value and any
- * conditions. The value must be a {@link Number}.
- *
- * @param value The value.
- * @return The {@link CellFormatPart} that applies to the value.
- */
- private CellFormatPart getApplicableFormatPart(Object value) {
-
- if (value instanceof Number) {
-
- double val = ((Number) value).doubleValue();
-
- if (formatPartCount == 1) {
- if (!posNumFmt.hasCondition()
- || (posNumFmt.hasCondition() && posNumFmt.applies(val))) {
- return posNumFmt;
- } else {
- return new CellFormatPart(locale, "General");
- }
- } else if (formatPartCount == 2) {
- if ((!posNumFmt.hasCondition() && val >= 0)
- || (posNumFmt.hasCondition() && posNumFmt.applies(val))) {
- return posNumFmt;
- } else if (!negNumFmt.hasCondition()
- || (negNumFmt.hasCondition() && negNumFmt.applies(val))) {
- return negNumFmt;
- } else {
- // Return ###...### (255 #s) to match Excel 2007 behaviour
- return new CellFormatPart(QUOTE + INVALID_VALUE_FOR_FORMAT + QUOTE);
- }
- } else {
- if ((!posNumFmt.hasCondition() && val > 0)
- || (posNumFmt.hasCondition() && posNumFmt.applies(val))) {
- return posNumFmt;
- } else if ((!negNumFmt.hasCondition() && val < 0)
- || (negNumFmt.hasCondition() && negNumFmt.applies(val))) {
- return negNumFmt;
- // Only the first two format parts can have conditions
- } else {
- return zeroNumFmt;
- }
- }
- } else {
- throw new IllegalArgumentException("value must be a Number");
- }
-
- }
-
- /**
- * Returns the ultimate cell type, following the results of formulas. If
- * the cell is a {@link CellType#FORMULA}, this returns the result of
- * {@link Cell#getCachedFormulaResultType()}. Otherwise this returns the
- * result of {@link Cell#getCellType()}.
- *
- * @param cell The cell.
- *
- * @return The ultimate type of this cell.
- */
- public static CellType ultimateType(Cell cell) {
- CellType type = cell.getCellType();
- if (type == CellType.FORMULA)
- return cell.getCachedFormulaResultType();
- else
- return type;
- }
-
- /**
- * Returns {@code true} if the other object is a CellFormat object
- * with the same format.
- *
- * @param obj The other object.
- *
- * @return {@code true} if the two objects are equal.
- */
- @Override
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj instanceof CellFormat) {
- CellFormat that = (CellFormat) obj;
- return format.equals(that.format);
- }
- return false;
- }
-
- /**
- * Returns a hash code for the format.
- *
- * @return A hash code for the format.
- */
- @Override
- public int hashCode() {
- return format.hashCode();
- }
- }
|