diff --git a/src/java/org/apache/poi/ss/format/CellNumberFormatter.java b/src/java/org/apache/poi/ss/format/CellNumberFormatter.java index b291cfd9a..8d3b31da2 100644 --- a/src/java/org/apache/poi/ss/format/CellNumberFormatter.java +++ b/src/java/org/apache/poi/ss/format/CellNumberFormatter.java @@ -19,52 +19,48 @@ package org.apache.poi.ss.format; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.FieldPosition; +import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.Formatter; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Set; import java.util.TreeSet; -import java.util.regex.Matcher; -import org.apache.poi.ss.format.CellFormatPart.PartHandler; import org.apache.poi.util.LocaleUtil; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; /** * This class implements printing out a value using a number format. - * - * @author Ken Arnold, Industrious Media LLC */ public class CellNumberFormatter extends CellFormatter { private static final POILogger LOG = POILogFactory.getLogger(CellNumberFormatter.class); - + private final String desc; - private String printfFmt; - private double scale; - private Special decimalPoint; - private Special slash; - private Special exponent; - private Special numerator; - private Special afterInteger; - private Special afterFractional; - private boolean integerCommas; - private final List specials; - private List integerSpecials; - private List fractionalSpecials; - private List numeratorSpecials; - private List denominatorSpecials; - private List exponentSpecials; - private List exponentDigitSpecials; - private int maxDenominator; - private String numeratorFmt; - private String denominatorFmt; - private boolean improperFraction; - private DecimalFormat decimalFmt; + private final String printfFmt; + private final double scale; + private final Special decimalPoint; + private final Special slash; + private final Special exponent; + private final Special numerator; + private final Special afterInteger; + private final Special afterFractional; + private final boolean integerCommas; + private final List specials = new ArrayList(); + private final List integerSpecials = new ArrayList(); + private final List fractionalSpecials = new ArrayList(); + private final List numeratorSpecials = new ArrayList(); + private final List denominatorSpecials = new ArrayList(); + private final List exponentSpecials = new ArrayList(); + private final List exponentDigitSpecials = new ArrayList(); + private final int maxDenominator; + private final String numeratorFmt; + private final String denominatorFmt; + private final boolean improperFraction; + private final DecimalFormat decimalFmt; // The CellNumberFormatter.simpleValue() method uses the SIMPLE_NUMBER // CellFormatter defined here. The CellFormat.GENERAL_FORMAT CellFormat @@ -73,19 +69,28 @@ public class CellNumberFormatter extends CellFormatter { // ("#" for integer values, and "#.#" for floating-point values) is // different from the 'General' format for numbers ("#" for integer // values and "#.#########" for floating-point values). - static final CellFormatter SIMPLE_NUMBER = new CellFormatter("General") { + private static final CellFormatter SIMPLE_NUMBER = new GeneralNumberFormatter(); + private static final CellFormatter SIMPLE_INT = new CellNumberFormatter("#"); + private static final CellFormatter SIMPLE_FLOAT = new CellNumberFormatter("#.#"); + + private static class GeneralNumberFormatter extends CellFormatter { + private GeneralNumberFormatter() { + super("General"); + } + public void formatValue(StringBuffer toAppendTo, Object value) { - if (value == null) + if (value == null) { return; + } + + CellFormatter cf; if (value instanceof Number) { Number num = (Number) value; - if (num.doubleValue() % 1.0 == 0) - SIMPLE_INT.formatValue(toAppendTo, value); - else - SIMPLE_FLOAT.formatValue(toAppendTo, value); + cf = (num.doubleValue() % 1.0 == 0) ? SIMPLE_INT : SIMPLE_FLOAT; } else { - CellTextFormatter.SIMPLE_TEXT.formatValue(toAppendTo, value); + cf = CellTextFormatter.SIMPLE_TEXT; } + cf.formatValue(toAppendTo, value); } public void simpleValue(StringBuffer toAppendTo, Object value) { @@ -93,16 +98,12 @@ public class CellNumberFormatter extends CellFormatter { } }; - private static final CellFormatter SIMPLE_INT = new CellNumberFormatter( - "#"); - private static final CellFormatter SIMPLE_FLOAT = new CellNumberFormatter( - "#.#"); /** * This class is used to mark where the special characters in the format * are, as opposed to the other characters that are simply printed. */ - static class Special { + /* package */ static class Special { final char ch; int pos; @@ -117,137 +118,6 @@ public class CellNumberFormatter extends CellFormatter { } } - /** - * This class represents a single modification to a result string. The way - * this works is complicated, but so is numeric formatting. In general, for - * most formats, we use a DecimalFormat object that will put the string out - * in a known format, usually with all possible leading and trailing zeros. - * We then walk through the result and the orginal format, and note any - * modifications that need to be made. Finally, we go through and apply - * them all, dealing with overlapping modifications. - */ - static class StringMod implements Comparable { - final Special special; - final int op; - CharSequence toAdd; - Special end; - boolean startInclusive; - boolean endInclusive; - - public static final int BEFORE = 1; - public static final int AFTER = 2; - public static final int REPLACE = 3; - - private StringMod(Special special, CharSequence toAdd, int op) { - this.special = special; - this.toAdd = toAdd; - this.op = op; - } - - public StringMod(Special start, boolean startInclusive, Special end, - boolean endInclusive, char toAdd) { - this(start, startInclusive, end, endInclusive); - this.toAdd = toAdd + ""; - } - - public StringMod(Special start, boolean startInclusive, Special end, - boolean endInclusive) { - special = start; - this.startInclusive = startInclusive; - this.end = end; - this.endInclusive = endInclusive; - op = REPLACE; - toAdd = ""; - } - - public int compareTo(StringMod that) { - int diff = special.pos - that.special.pos; - if (diff != 0) - return diff; - else - return op - that.op; - } - - @Override - public boolean equals(Object that) { - try { - return compareTo((StringMod) that) == 0; - } catch (RuntimeException ignored) { - // NullPointerException or CastException - return false; - } - } - - @Override - public int hashCode() { - return special.hashCode() + op; - } - } - - private class NumPartHandler implements PartHandler { - private char insertSignForExponent; - - public String handlePart(Matcher m, String part, CellFormatType type, - StringBuffer descBuf) { - int pos = descBuf.length(); - char firstCh = part.charAt(0); - switch (firstCh) { - case 'e': - case 'E': - // See comment in writeScientific -- exponent handling is complex. - // (1) When parsing the format, remove the sign from after the 'e' and - // put it before the first digit of the exponent. - if (exponent == null && specials.size() > 0) { - specials.add(exponent = new Special('.', pos)); - insertSignForExponent = part.charAt(1); - return part.substring(0, 1); - } - break; - - case '0': - case '?': - case '#': - if (insertSignForExponent != '\0') { - specials.add(new Special(insertSignForExponent, pos)); - descBuf.append(insertSignForExponent); - insertSignForExponent = '\0'; - pos++; - } - for (int i = 0; i < part.length(); i++) { - char ch = part.charAt(i); - specials.add(new Special(ch, pos + i)); - } - break; - - case '.': - if (decimalPoint == null && specials.size() > 0) - specials.add(decimalPoint = new Special('.', pos)); - break; - - case '/': - //!! This assumes there is a numerator and a denominator, but these are actually optional - if (slash == null && specials.size() > 0) { - numerator = previousNumber(); - // If the first number in the whole format is the numerator, the - // entire number should be printed as an improper fraction - if (numerator == firstDigit(specials)) - improperFraction = true; - specials.add(slash = new Special('.', pos)); - } - break; - - case '%': - // don't need to remember because we don't need to do anything with these - scale *= 100; - break; - - default: - return null; - } - return part; - } - } - /** * Creates a new cell number formatter. * @@ -256,71 +126,100 @@ public class CellNumberFormatter extends CellFormatter { public CellNumberFormatter(String format) { super(format); - scale = 1; + CellNumberPartHandler ph = new CellNumberPartHandler(); + StringBuffer descBuf = CellFormatPart.parseFormat(format, CellFormatType.NUMBER, ph); - specials = new LinkedList(); - - NumPartHandler partHandler = new NumPartHandler(); - StringBuffer descBuf = CellFormatPart.parseFormat(format, - CellFormatType.NUMBER, partHandler); + exponent = ph.getExponent(); + specials.addAll(ph.getSpecials()); + improperFraction = ph.isImproperFraction(); // These are inconsistent settings, so ditch 'em - if ((decimalPoint != null || exponent != null) && slash != null) { + if ((ph.getDecimalPoint() != null || ph.getExponent() != null) && ph.getSlash() != null) { slash = null; numerator = null; + } else { + slash = ph.getSlash(); + numerator = ph.getNumerator(); } - interpretCommas(descBuf); - - int precision; + final int precision = interpretPrecision(ph.getDecimalPoint(), specials); int fractionPartWidth = 0; - if (decimalPoint == null) { - precision = 0; - } else { - precision = interpretPrecision(); + if (ph.getDecimalPoint() != null) { fractionPartWidth = 1 + precision; if (precision == 0) { // This means the format has a ".", but that output should have no decimals after it. // We just stop treating it specially - specials.remove(decimalPoint); + specials.remove(ph.getDecimalPoint()); decimalPoint = null; + } else { + decimalPoint = ph.getDecimalPoint(); } - } - - if (precision == 0) - fractionalSpecials = Collections.emptyList(); - else - fractionalSpecials = specials.subList(specials.indexOf( - decimalPoint) + 1, fractionalEnd()); - if (exponent == null) - exponentSpecials = Collections.emptyList(); - else { - int exponentPos = specials.indexOf(exponent); - exponentSpecials = specialsFor(exponentPos, 2); - exponentDigitSpecials = specialsFor(exponentPos + 2); - } - - if (slash == null) { - numeratorSpecials = Collections.emptyList(); - denominatorSpecials = Collections.emptyList(); } else { - if (numerator == null) - numeratorSpecials = Collections.emptyList(); - else - numeratorSpecials = specialsFor(specials.indexOf(numerator)); + decimalPoint = null; + } - denominatorSpecials = specialsFor(specials.indexOf(slash) + 1); + if (decimalPoint != null) { + afterInteger = decimalPoint; + } else if (exponent != null) { + afterInteger = exponent; + } else if (numerator != null) { + afterInteger = numerator; + } else { + afterInteger = null; + } + + if (exponent != null) { + afterFractional = exponent; + } else if (numerator != null) { + afterFractional = numerator; + } else { + afterFractional = null; + } + + double scaleByRef[] = { ph.getScale() }; + integerCommas = interpretIntegerCommas(descBuf, specials, decimalPoint, integerEnd(), fractionalEnd(), scaleByRef); + if (exponent == null) { + scale = scaleByRef[0]; + } else { + // in "e" formats,% and trailing commas have no scaling effect + scale = 1; + } + + if (precision != 0) { + // TODO: if decimalPoint is null (-> index == -1), return the whole list? + fractionalSpecials.addAll(specials.subList(specials.indexOf(decimalPoint) + 1, fractionalEnd())); + } + + if (exponent != null) { + int exponentPos = specials.indexOf(exponent); + exponentSpecials.addAll(specialsFor(exponentPos, 2)); + exponentDigitSpecials.addAll(specialsFor(exponentPos + 2)); + } + + if (slash != null) { + if (numerator != null) { + numeratorSpecials.addAll(specialsFor(specials.indexOf(numerator))); + } + + denominatorSpecials.addAll(specialsFor(specials.indexOf(slash) + 1)); if (denominatorSpecials.isEmpty()) { // no denominator follows the slash, drop the fraction idea - numeratorSpecials = Collections.emptyList(); + numeratorSpecials.clear(); + maxDenominator = 1; + numeratorFmt = null; + denominatorFmt = null; } else { maxDenominator = maxValue(denominatorSpecials); numeratorFmt = singleNumberFormat(numeratorSpecials); denominatorFmt = singleNumberFormat(denominatorSpecials); } + } else { + maxDenominator = 1; + numeratorFmt = null; + denominatorFmt = null; } - integerSpecials = specials.subList(0, integerEnd()); + integerSpecials.addAll(specials.subList(0, integerEnd())); if (exponent == null) { StringBuffer fmtBuf = new StringBuffer("%"); @@ -332,6 +231,7 @@ public class CellNumberFormatter extends CellFormatter { fmtBuf.append("f"); printfFmt = fmtBuf.toString(); + decimalFmt = null; } else { StringBuffer fmtBuf = new StringBuffer(); boolean first = true; @@ -358,49 +258,33 @@ public class CellNumberFormatter extends CellFormatter { } } fmtBuf.append('E'); - placeZeros(fmtBuf, exponentSpecials.subList(2, - exponentSpecials.size())); + placeZeros(fmtBuf, exponentSpecials.subList(2, exponentSpecials.size())); DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(LocaleUtil.getUserLocale()); decimalFmt = new DecimalFormat(fmtBuf.toString(), dfs); + printfFmt = null; } - if (exponent != null) - scale = - 1; // in "e" formats,% and trailing commas have no scaling effect - desc = descBuf.toString(); } private static void placeZeros(StringBuffer sb, List specials) { for (Special s : specials) { - if (isDigitFmt(s)) + if (isDigitFmt(s)) { sb.append('0'); + } } } - private static Special firstDigit(List specials) { - for (Special s : specials) { - if (isDigitFmt(s)) - return s; - } - return null; + private static CellNumberStringMod insertMod(Special special, CharSequence toAdd, int where) { + return new CellNumberStringMod(special, toAdd, where); } - static StringMod insertMod(Special special, CharSequence toAdd, int where) { - return new StringMod(special, toAdd, where); + private static CellNumberStringMod deleteMod(Special start, boolean startInclusive, Special end, boolean endInclusive) { + return new CellNumberStringMod(start, startInclusive, end, endInclusive); } - static StringMod deleteMod(Special start, boolean startInclusive, - Special end, boolean endInclusive) { - - return new StringMod(start, startInclusive, end, endInclusive); - } - - static StringMod replaceMod(Special start, boolean startInclusive, - Special end, boolean endInclusive, char withChar) { - - return new StringMod(start, startInclusive, end, endInclusive, - withChar); + private static CellNumberStringMod replaceMod(Special start, boolean startInclusive, Special end, boolean endInclusive, char withChar) { + return new CellNumberStringMod(start, startInclusive, end, endInclusive, withChar); } private static String singleNumberFormat(List numSpecials) { @@ -412,8 +296,9 @@ public class CellNumberFormatter extends CellFormatter { } private List specialsFor(int pos, int takeFirst) { - if (pos >= specials.size()) + if (pos >= specials.size()) { return Collections.emptyList(); + } ListIterator it = specials.listIterator(pos + takeFirst); Special last = it.next(); int end = pos + takeFirst; @@ -435,76 +320,50 @@ public class CellNumberFormatter extends CellFormatter { return s.ch == '0' || s.ch == '?' || s.ch == '#'; } - private Special previousNumber() { - ListIterator it = specials.listIterator(specials.size()); - while (it.hasPrevious()) { - Special s = it.previous(); - if (isDigitFmt(s)) { - Special numStart = s; - Special last = s; - while (it.hasPrevious()) { - s = it.previous(); - if (last.pos - s.pos > 1) // it has to be continuous digits - break; - if (isDigitFmt(s)) - numStart = s; - else - break; - last = s; - } - return numStart; - } - } - return null; - } - private int calculateIntegerPartWidth() { - ListIterator it = specials.listIterator(); int digitCount = 0; - while (it.hasNext()) { - Special s = it.next(); + for (Special s : specials) { //!! Handle fractions: The previous set of digits before that is the numerator, so we should stop short of that - if (s == afterInteger) + if (s == afterInteger) { break; - else if (isDigitFmt(s)) + } else if (isDigitFmt(s)) { digitCount++; + } } return digitCount; } - private int interpretPrecision() { - if (decimalPoint == null) { - return -1; - } else { - int precision = 0; - ListIterator it = specials.listIterator(specials.indexOf( - decimalPoint)); - if (it.hasNext()) - it.next(); // skip over the decimal point itself + private static int interpretPrecision(Special decimalPoint, List specials) { + int idx = specials.indexOf(decimalPoint); + int precision = 0; + if (idx != -1) { + // skip over the decimal point itself + ListIterator it = specials.listIterator(idx+1); while (it.hasNext()) { Special s = it.next(); - if (isDigitFmt(s)) - precision++; - else + if (!isDigitFmt(s)) { break; + } + precision++; } - return precision; } + return precision; } - private void interpretCommas(StringBuffer sb) { + private static boolean interpretIntegerCommas + (StringBuffer sb, List specials, Special decimalPoint, int integerEnd, int fractionalEnd, double[] scale) { // In the integer part, commas at the end are scaling commas; other commas mean to show thousand-grouping commas - ListIterator it = specials.listIterator(integerEnd()); + ListIterator it = specials.listIterator(integerEnd); boolean stillScaling = true; - integerCommas = false; + boolean integerCommas = false; while (it.hasPrevious()) { Special s = it.previous(); if (s.ch != ',') { stillScaling = false; } else { if (stillScaling) { - scale /= 1000; + scale[0] /= 1000; } else { integerCommas = true; } @@ -512,13 +371,13 @@ public class CellNumberFormatter extends CellFormatter { } if (decimalPoint != null) { - it = specials.listIterator(fractionalEnd()); + it = specials.listIterator(fractionalEnd); while (it.hasPrevious()) { Special s = it.previous(); if (s.ch != ',') { break; } else { - scale /= 1000; + scale[0] /= 1000; } } } @@ -535,32 +394,16 @@ public class CellNumberFormatter extends CellFormatter { sb.deleteCharAt(s.pos); } } + + return integerCommas; } private int integerEnd() { - if (decimalPoint != null) - afterInteger = decimalPoint; - else if (exponent != null) - afterInteger = exponent; - else if (numerator != null) - afterInteger = numerator; - else - afterInteger = null; - return afterInteger == null ? specials.size() : specials.indexOf( - afterInteger); + return (afterInteger == null) ? specials.size() : specials.indexOf(afterInteger); } private int fractionalEnd() { - int end; - if (exponent != null) - afterFractional = exponent; - else if (numerator != null) - afterInteger = numerator; - else - afterFractional = null; - end = afterFractional == null ? specials.size() : specials.indexOf( - afterFractional); - return end; + return (afterFractional == null) ? specials.size() : specials.indexOf(afterFractional); } /** {@inheritDoc} */ @@ -592,7 +435,7 @@ public class CellNumberFormatter extends CellFormatter { } } - Set mods = new TreeSet(); + Set mods = new TreeSet(); StringBuffer output = new StringBuffer(desc); if (exponent != null) { @@ -610,46 +453,44 @@ public class CellNumberFormatter extends CellFormatter { if (numerator == null) { writeFractional(result, output); - writeInteger(result, output, integerSpecials, mods, - integerCommas); + writeInteger(result, output, integerSpecials, mods, integerCommas); } else { writeFraction(value, result, fractional, output, mods); } } // Now strip out any remaining '#'s and add any pending text ... - ListIterator it = specials.listIterator(); - Iterator changes = mods.iterator(); - StringMod nextChange = (changes.hasNext() ? changes.next() : null); + Iterator changes = mods.iterator(); + CellNumberStringMod nextChange = (changes.hasNext() ? changes.next() : null); + // records chars already deleted + BitSet deletedChars = new BitSet(); int adjust = 0; - BitSet deletedChars = new BitSet(); // records chars already deleted - while (it.hasNext()) { - Special s = it.next(); + for (Special s : specials) { int adjustedPos = s.pos + adjust; if (!deletedChars.get(s.pos) && output.charAt(adjustedPos) == '#') { output.deleteCharAt(adjustedPos); adjust--; deletedChars.set(s.pos); } - while (nextChange != null && s == nextChange.special) { + while (nextChange != null && s == nextChange.getSpecial()) { int lenBefore = output.length(); int modPos = s.pos + adjust; - int posTweak = 0; - switch (nextChange.op) { - case StringMod.AFTER: + switch (nextChange.getOp()) { + case CellNumberStringMod.AFTER: // ignore adding a comma after a deleted char (which was a '#') - if (nextChange.toAdd.equals(",") && deletedChars.get(s.pos)) + if (nextChange.getToAdd().equals(",") && deletedChars.get(s.pos)) { break; - posTweak = 1; - //noinspection fallthrough - case StringMod.BEFORE: - output.insert(modPos + posTweak, nextChange.toAdd); + } + output.insert(modPos + 1, nextChange.getToAdd()); + break; + case CellNumberStringMod.BEFORE: + output.insert(modPos, nextChange.getToAdd()); break; - case StringMod.REPLACE: - int delPos = - s.pos; // delete starting pos in original coordinates - if (!nextChange.startInclusive) { + case CellNumberStringMod.REPLACE: + // delete starting pos in original coordinates + int delPos = s.pos; + if (!nextChange.isStartInclusive()) { delPos++; modPos++; } @@ -660,51 +501,49 @@ public class CellNumberFormatter extends CellFormatter { modPos++; } - int delEndPos = - nextChange.end.pos; // delete end point in original - if (nextChange.endInclusive) + // delete end point in original + int delEndPos = nextChange.getEnd().pos; + if (nextChange.isEndInclusive()) { delEndPos++; + } - int modEndPos = - delEndPos + adjust; // delete end point in current + // delete end point in current + int modEndPos = delEndPos + adjust; if (modPos < modEndPos) { - if ("".equals(nextChange.toAdd)) + if ("".equals(nextChange.getToAdd())) { output.delete(modPos, modEndPos); + } else { - char fillCh = nextChange.toAdd.charAt(0); - for (int i = modPos; i < modEndPos; i++) + char fillCh = nextChange.getToAdd().charAt(0); + for (int i = modPos; i < modEndPos; i++) { output.setCharAt(i, fillCh); + } } deletedChars.set(delPos, delEndPos); } break; default: - throw new IllegalStateException( - "Unknown op: " + nextChange.op); + throw new IllegalStateException("Unknown op: " + nextChange.getOp()); } adjust += output.length() - lenBefore; - if (changes.hasNext()) - nextChange = changes.next(); - else - nextChange = null; + nextChange = (changes.hasNext()) ? changes.next() : null; } } // Finally, add it to the string - if (negative) + if (negative) { toAppendTo.append('-'); + } toAppendTo.append(output); } - private void writeScientific(double value, StringBuffer output, - Set mods) { + private void writeScientific(double value, StringBuffer output, Set mods) { StringBuffer result = new StringBuffer(); - FieldPosition fractionPos = new FieldPosition( - DecimalFormat.FRACTION_FIELD); + FieldPosition fractionPos = new FieldPosition(DecimalFormat.FRACTION_FIELD); decimalFmt.format(value, result, fractionPos); writeInteger(result, output, integerSpecials, mods, integerCommas); writeFractional(result, output); @@ -764,19 +603,19 @@ public class CellNumberFormatter extends CellFormatter { // (4) In the output, remove the sign if it should not be shown or set it to // the correct value. - if (expSignRes == '-' || expSignFmt == '+') + if (expSignRes == '-' || expSignFmt == '+') { mods.add(replaceMod(expSign, true, expSign, true, expSignRes)); - else + } else { mods.add(deleteMod(expSign, true, expSign, true)); + } - StringBuffer exponentNum = new StringBuffer(result.substring( - signPos + 1)); + StringBuffer exponentNum = new StringBuffer(result.substring(signPos + 1)); writeInteger(exponentNum, output, exponentDigitSpecials, mods, false); } @SuppressWarnings("unchecked") private void writeFraction(double value, StringBuffer result, - double fractional, StringBuffer output, Set mods) { + double fractional, StringBuffer output, Set mods) { // Figure out if we are to suppress either the integer or fractional part. // With # the suppressed part is removed; with ? it is replaced with spaces. @@ -786,11 +625,9 @@ public class CellNumberFormatter extends CellFormatter { if (fractional == 0 && !hasChar('0', numeratorSpecials)) { writeInteger(result, output, integerSpecials, mods, false); - Special start = integerSpecials.get(integerSpecials.size() - 1); - Special end = denominatorSpecials.get( - denominatorSpecials.size() - 1); - if (hasChar('?', integerSpecials, numeratorSpecials, - denominatorSpecials)) { + Special start = lastSpecial(integerSpecials); + Special end = lastSpecial(denominatorSpecials); + if (hasChar('?', integerSpecials, numeratorSpecials, denominatorSpecials)) { //if any format has '?', then replace the fraction with spaces mods.add(replaceMod(start, false, end, true, ' ')); } else { @@ -802,23 +639,20 @@ public class CellNumberFormatter extends CellFormatter { return; } else { // New we check to see if we should remove the integer part - boolean allZero = (value == 0 && fractional == 0); - boolean willShowFraction = fractional != 0 || hasChar('0', - numeratorSpecials); - boolean removeBecauseZero = allZero && (hasOnly('#', - integerSpecials) || !hasChar('0', numeratorSpecials)); - boolean removeBecauseFraction = - !allZero && value == 0 && willShowFraction && !hasChar( - '0', integerSpecials); - if (removeBecauseZero || removeBecauseFraction) { - Special start = integerSpecials.get( - integerSpecials.size() - 1); - if (hasChar('?', integerSpecials, numeratorSpecials)) { - mods.add(replaceMod(start, true, numerator, false, - ' ')); - } else { - mods.add(deleteMod(start, true, numerator, false)); - } + boolean numNoZero = !numeratorSpecials.contains('0'); + boolean intNoZero = !integerSpecials.contains('0'); + boolean intOnlyHash = integerSpecials.isEmpty() || (integerSpecials.size() == 1 && integerSpecials.contains('#')); + + boolean removeBecauseZero = fractional == 0 && (intOnlyHash || numNoZero); + boolean removeBecauseFraction = fractional != 0 && intNoZero; + + if (value == 0 && (removeBecauseZero || removeBecauseFraction)) { + Special start = lastSpecial(integerSpecials); + boolean hasPlaceHolder = integerSpecials.contains('?') || numeratorSpecials.contains('?'); + CellNumberStringMod sm = hasPlaceHolder + ? replaceMod(start, true, numerator, false, ' ') + : deleteMod(start, true, numerator, false); + mods.add(sm); } else { // Not removing the integer part -- print it out writeInteger(result, output, integerSpecials, mods, false); @@ -840,12 +674,11 @@ public class CellNumberFormatter extends CellFormatter { n = frac.getNumerator(); d = frac.getDenominator(); } - if (improperFraction) + if (improperFraction) { n += Math.round(value * d); - writeSingleInteger(numeratorFmt, n, output, numeratorSpecials, - mods); - writeSingleInteger(denominatorFmt, d, output, denominatorSpecials, - mods); + } + writeSingleInteger(numeratorFmt, n, output, numeratorSpecials, mods); + writeSingleInteger(denominatorFmt, d, output, denominatorSpecials, mods); } catch (RuntimeException ignored) { LOG.log(POILogger.ERROR, "error while fraction evaluation", ignored); } @@ -862,19 +695,7 @@ public class CellNumberFormatter extends CellFormatter { return false; } - private static boolean hasOnly(char ch, List... numSpecials) { - for (List specials : numSpecials) { - for (Special s : specials) { - if (s.ch != ch) { - return false; - } - } - } - return true; - } - - private void writeSingleInteger(String fmt, int num, StringBuffer output, - List numSpecials, Set mods) { + private void writeSingleInteger(String fmt, int num, StringBuffer output, List numSpecials, Set mods) { StringBuffer sb = new StringBuffer(); Formatter formatter = new Formatter(sb, LocaleUtil.getUserLocale()); @@ -887,22 +708,24 @@ public class CellNumberFormatter extends CellFormatter { } private void writeInteger(StringBuffer result, StringBuffer output, - List numSpecials, Set mods, + List numSpecials, Set mods, boolean showCommas) { int pos = result.indexOf(".") - 1; if (pos < 0) { - if (exponent != null && numSpecials == integerSpecials) + if (exponent != null && numSpecials == integerSpecials) { pos = result.indexOf("E") - 1; - else + } else { pos = result.length() - 1; + } } int strip; for (strip = 0; strip < pos; strip++) { char resultCh = result.charAt(strip); - if (resultCh != '0' && resultCh != ',') + if (resultCh != '0' && resultCh != ',') { break; + } } ListIterator it = numSpecials.listIterator(numSpecials.size()); @@ -911,9 +734,9 @@ public class CellNumberFormatter extends CellFormatter { int digit = 0; while (it.hasPrevious()) { char resultCh; - if (pos >= 0) + if (pos >= 0) { resultCh = result.charAt(pos); - else { + } else { // If result is shorter than field, pretend there are leading zeros resultCh = '0'; } @@ -926,7 +749,7 @@ public class CellNumberFormatter extends CellFormatter { lastOutputIntegerDigit = s; } if (followWithComma) { - mods.add(insertMod(s, zeroStrip ? " " : ",", StringMod.AFTER)); + mods.add(insertMod(s, zeroStrip ? " " : ",", CellNumberStringMod.AFTER)); followWithComma = false; } digit++; @@ -935,41 +758,44 @@ public class CellNumberFormatter extends CellFormatter { StringBuffer extraLeadingDigits = new StringBuffer(); if (pos >= 0) { // We ran out of places to put digits before we ran out of digits; put this aside so we can add it later - ++pos; // pos was decremented at the end of the loop above when the iterator was at its end + // pos was decremented at the end of the loop above when the iterator was at its end + ++pos; extraLeadingDigits = new StringBuffer(result.substring(0, pos)); if (showCommas) { while (pos > 0) { - if (digit > 0 && digit % 3 == 0) + if (digit > 0 && digit % 3 == 0) { extraLeadingDigits.insert(pos, ','); + } digit++; --pos; } } - mods.add(insertMod(lastOutputIntegerDigit, extraLeadingDigits, - StringMod.BEFORE)); + mods.add(insertMod(lastOutputIntegerDigit, extraLeadingDigits, CellNumberStringMod.BEFORE)); } } private void writeFractional(StringBuffer result, StringBuffer output) { int digit; int strip; - ListIterator it; if (fractionalSpecials.size() > 0) { digit = result.indexOf(".") + 1; - if (exponent != null) + if (exponent != null) { strip = result.indexOf("e") - 1; - else + } else { strip = result.length() - 1; - while (strip > digit && result.charAt(strip) == '0') + } + + while (strip > digit && result.charAt(strip) == '0') { strip--; - it = fractionalSpecials.listIterator(); - while (it.hasNext()) { - Special s = it.next(); + } + + for (Special s : fractionalSpecials) { char resultCh = result.charAt(digit); - if (resultCh != '0' || s.ch == '0' || digit < strip) + if (resultCh != '0' || s.ch == '0' || digit < strip) { output.setCharAt(s.pos, resultCh); - else if (s.ch == '?') { - // This is when we're in trailing zeros, and the format is '?'. We still strip out remaining '#'s later + } else if (s.ch == '?') { + // This is when we're in trailing zeros, and the format is '?'. + // We still strip out remaining '#'s later output.setCharAt(s.pos, ' '); } digit++; @@ -987,6 +813,7 @@ public class CellNumberFormatter extends CellFormatter { SIMPLE_NUMBER.formatValue(toAppendTo, value); } - - + private static Special lastSpecial(List s) { + return s.get(s.size() - 1); + } } diff --git a/src/java/org/apache/poi/ss/format/CellNumberPartHandler.java b/src/java/org/apache/poi/ss/format/CellNumberPartHandler.java new file mode 100644 index 000000000..605f8ff6c --- /dev/null +++ b/src/java/org/apache/poi/ss/format/CellNumberPartHandler.java @@ -0,0 +1,167 @@ +/* ==================================================================== + 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.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.regex.Matcher; + +import org.apache.poi.ss.format.CellFormatPart.PartHandler; +import org.apache.poi.ss.format.CellNumberFormatter.Special; +import org.apache.poi.util.Internal; + +/** + * Internal helper class for CellNumberFormatter + */ +@Internal +public class CellNumberPartHandler implements PartHandler { + private char insertSignForExponent; + private double scale = 1; + private Special decimalPoint; + private Special slash; + private Special exponent; + private Special numerator; + private final List specials = new LinkedList(); + private boolean improperFraction; + + public String handlePart(Matcher m, String part, CellFormatType type, StringBuffer descBuf) { + int pos = descBuf.length(); + char firstCh = part.charAt(0); + switch (firstCh) { + case 'e': + case 'E': + // See comment in writeScientific -- exponent handling is complex. + // (1) When parsing the format, remove the sign from after the 'e' and + // put it before the first digit of the exponent. + if (exponent == null && specials.size() > 0) { + specials.add(exponent = new Special('.', pos)); + insertSignForExponent = part.charAt(1); + return part.substring(0, 1); + } + break; + + case '0': + case '?': + case '#': + if (insertSignForExponent != '\0') { + specials.add(new Special(insertSignForExponent, pos)); + descBuf.append(insertSignForExponent); + insertSignForExponent = '\0'; + pos++; + } + for (int i = 0; i < part.length(); i++) { + char ch = part.charAt(i); + specials.add(new Special(ch, pos + i)); + } + break; + + case '.': + if (decimalPoint == null && specials.size() > 0) + specials.add(decimalPoint = new Special('.', pos)); + break; + + case '/': + //!! This assumes there is a numerator and a denominator, but these are actually optional + if (slash == null && specials.size() > 0) { + numerator = previousNumber(); + // If the first number in the whole format is the numerator, the + // entire number should be printed as an improper fraction + if (numerator == firstDigit(specials)) + improperFraction = true; + specials.add(slash = new Special('.', pos)); + } + break; + + case '%': + // don't need to remember because we don't need to do anything with these + scale *= 100; + break; + + default: + return null; + } + return part; + } + + public char getInsertSignForExponent() { + return insertSignForExponent; + } + + public double getScale() { + return scale; + } + + public Special getDecimalPoint() { + return decimalPoint; + } + + public Special getSlash() { + return slash; + } + + public Special getExponent() { + return exponent; + } + + public Special getNumerator() { + return numerator; + } + + public List getSpecials() { + return specials; + } + + public boolean isImproperFraction() { + return improperFraction; + } + + private Special previousNumber() { + ListIterator it = specials.listIterator(specials.size()); + while (it.hasPrevious()) { + Special s = it.previous(); + if (isDigitFmt(s)) { + Special numStart = s; + Special last = s; + while (it.hasPrevious()) { + s = it.previous(); + if (last.pos - s.pos > 1) // it has to be continuous digits + break; + if (isDigitFmt(s)) + numStart = s; + else + break; + last = s; + } + return numStart; + } + } + return null; + } + + private static boolean isDigitFmt(Special s) { + return s.ch == '0' || s.ch == '?' || s.ch == '#'; + } + + private static Special firstDigit(List specials) { + for (Special s : specials) { + if (isDigitFmt(s)) + return s; + } + return null; + } +} diff --git a/src/java/org/apache/poi/ss/format/CellNumberStringMod.java b/src/java/org/apache/poi/ss/format/CellNumberStringMod.java new file mode 100644 index 000000000..a053206a8 --- /dev/null +++ b/src/java/org/apache/poi/ss/format/CellNumberStringMod.java @@ -0,0 +1,109 @@ +/* ==================================================================== + 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 org.apache.poi.ss.format.CellNumberFormatter.Special; +import org.apache.poi.util.Internal; + +/** + * Internal helper class for CellNumberFormatter + * + * This class represents a single modification to a result string. The way + * this works is complicated, but so is numeric formatting. In general, for + * most formats, we use a DecimalFormat object that will put the string out + * in a known format, usually with all possible leading and trailing zeros. + * We then walk through the result and the original format, and note any + * modifications that need to be made. Finally, we go through and apply + * them all, dealing with overlapping modifications. + */ +@Internal +public class CellNumberStringMod implements Comparable { + public static final int BEFORE = 1; + public static final int AFTER = 2; + public static final int REPLACE = 3; + + private final Special special; + private final int op; + private CharSequence toAdd; + private Special end; + private boolean startInclusive; + private boolean endInclusive; + + public CellNumberStringMod(Special special, CharSequence toAdd, int op) { + this.special = special; + this.toAdd = toAdd; + this.op = op; + } + + public CellNumberStringMod(Special start, boolean startInclusive, Special end, boolean endInclusive, char toAdd) { + this(start, startInclusive, end, endInclusive); + this.toAdd = toAdd + ""; + } + + public CellNumberStringMod(Special start, boolean startInclusive, Special end, boolean endInclusive) { + special = start; + this.startInclusive = startInclusive; + this.end = end; + this.endInclusive = endInclusive; + op = REPLACE; + toAdd = ""; + } + + public int compareTo(CellNumberStringMod that) { + int diff = special.pos - that.special.pos; + return (diff != 0) ? diff : (op - that.op); + } + + @Override + public boolean equals(Object that) { + try { + return compareTo((CellNumberStringMod) that) == 0; + } catch (RuntimeException ignored) { + // NullPointerException or CastException + return false; + } + } + + @Override + public int hashCode() { + return special.hashCode() + op; + } + + public Special getSpecial() { + return special; + } + + public int getOp() { + return op; + } + + public CharSequence getToAdd() { + return toAdd; + } + + public Special getEnd() { + return end; + } + + public boolean isStartInclusive() { + return startInclusive; + } + + public boolean isEndInclusive() { + return endInclusive; + } +}