CellNumberFormatter.java
/* ====================================================================
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.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.IllegalFormatException;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;
import com.zaxxer.sparsebits.SparseBitSet;
import org.apache.logging.log4j.Logger;
import org.apache.poi.logging.PoiLogManager;
import org.apache.poi.util.LocaleUtil;
/**
* This class implements printing out a value using a number format.
*/
public class CellNumberFormatter extends CellFormatter {
private static final Logger LOG = PoiLogManager.getLogger(CellNumberFormatter.class);
// the largest numerator Excel shows for an improper fraction, the maximum unsigned 32-bit int
private static final long MAX_IMPROPER_NUMERATOR = 0xFFFFFFFFL;
private final String desc;
private final String printfFmt;
private final double scale;
private final Special decimalPoint;
/** where the decimal point is in {@link #desc}, shown as the locale's decimal separator, or -1 */
private final int decimalPointPos;
private final Special slash;
private final Special exponent;
private final Special numerator;
private final Special afterInteger;
private final Special afterFractional;
private final boolean showGroupingSeparator;
private final List<Special> specials = new ArrayList<>();
private final List<Special> integerSpecials = new ArrayList<>();
private final List<Special> fractionalSpecials = new ArrayList<>();
private final List<Special> numeratorSpecials = new ArrayList<>();
private final List<Special> denominatorSpecials = new ArrayList<>();
private final List<Special> exponentSpecials = new ArrayList<>();
private final List<Special> exponentDigitSpecials = new ArrayList<>();
private final int maxDenominator;
/** the denominator given as a number, e.g. 4 for "# ?/4", or 0 */
private final int fixedDenominator;
/** the last digit of the fixed denominator, which ends the fraction */
private final Special fixedDenominatorEnd;
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
// no longer uses the SIMPLE_NUMBER CellFormatter.
// Note that the simpleValue()/SIMPLE_NUMBER CellFormatter format
// ("#" for integer values, and "#.#" for floating-point values) is
// different from the 'General' format for numbers ("#" for integer
// values and "#.#########" for floating-point values).
private final CellFormatter SIMPLE_NUMBER = new GeneralNumberFormatter(locale);
private static class GeneralNumberFormatter extends CellFormatter {
private GeneralNumberFormatter(Locale locale) {
super(locale, "General");
}
@Override
public void formatValue(StringBuffer toAppendTo, Object value) {
if (value == null) {
return;
}
CellFormatter cf;
if (value instanceof Number num) {
cf = (num.doubleValue() % 1.0 == 0) ? new CellNumberFormatter(locale, "#") :
new CellNumberFormatter(locale, "#.#");
} else {
cf = CellTextFormatter.SIMPLE_TEXT;
}
cf.formatValue(toAppendTo, value);
}
@Override
public void simpleValue(StringBuffer toAppendTo, Object value) {
formatValue(toAppendTo, value);
}
}
/**
* This class is used to mark where the special characters in the format
* are, as opposed to the other characters that are simply printed.
*/
/* package */ static class Special {
final char ch;
int pos;
Special(char ch, int pos) {
this.ch = ch;
this.pos = pos;
}
@Override
public String toString() {
return "'" + ch + "' @ " + pos;
}
}
/**
* Creates a new cell number formatter.
*
* @param format The format to parse.
*/
public CellNumberFormatter(String format) {
this(LocaleUtil.getUserLocale(), format);
}
/**
* Creates a new cell number formatter.
*
* @param locale The locale to use.
* @param format The format to parse.
*/
public CellNumberFormatter(Locale locale, String format) {
super(locale, format);
CellNumberPartHandler ph = new CellNumberPartHandler();
StringBuffer descBuf = CellFormatPart.parseFormat(format, CellFormatType.NUMBER, ph);
exponent = ph.getExponent();
specials.addAll(ph.getSpecials());
improperFraction = ph.isImproperFraction();
// These are inconsistent settings, so ditch 'em
if ((ph.getDecimalPoint() != null || ph.getExponent() != null) && ph.getSlash() != null) {
slash = null;
numerator = null;
} else {
slash = ph.getSlash();
numerator = ph.getNumerator();
}
final int precision = interpretPrecision(ph.getDecimalPoint(), specials);
int fractionPartWidth = 0;
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(ph.getDecimalPoint());
decimalPoint = null;
} else {
decimalPoint = ph.getDecimalPoint();
}
} else {
decimalPoint = null;
}
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;
}
// the decimal point moves left by one for each comma before it, which is removed below
int pointPos = -1;
if (ph.getDecimalPoint() != null) {
pointPos = ph.getDecimalPoint().pos;
for (Special s : specials) {
if (s.ch == ',' && s.pos < ph.getDecimalPoint().pos) {
pointPos--;
}
}
}
decimalPointPos = pointPos;
double[] scaleByRef = {ph.getScale()};
showGroupingSeparator = 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));
}
fixedDenominator = slash == null ? 0 : ph.getFixedDenominator();
fixedDenominatorEnd = slash == null ? null : ph.getFixedDenominatorEnd();
if (slash != null && fixedDenominator > 0) {
if (numerator != null) {
numeratorSpecials.addAll(specialsFor(specials.indexOf(numerator)));
}
maxDenominator = fixedDenominator;
numeratorFmt = singleNumberFormat(numeratorSpecials);
denominatorFmt = null;
} else 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.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.addAll(specials.subList(0, integerEnd()));
if (exponent == null) {
int integerPartWidth = calculateIntegerPartWidth();
int totalWidth = integerPartWidth + fractionPartWidth;
// need to handle empty width specially as %00.0f fails during formatting
if(totalWidth == 0) {
printfFmt = "";
} else {
printfFmt = "%0" + totalWidth + '.' + precision + "f";
}
decimalFmt = null;
} else {
StringBuffer fmtBuf = new StringBuffer();
boolean first = true;
if (integerSpecials.size() == 1) {
// If we don't do this, we get ".6e5" instead of "6e4"
fmtBuf.append("0");
first = false;
} else
for (Special s : integerSpecials) {
if (isDigitFmt(s)) {
fmtBuf.append(first ? '#' : '0');
first = false;
}
}
if (!fractionalSpecials.isEmpty()) {
fmtBuf.append('.');
for (Special s : fractionalSpecials) {
if (isDigitFmt(s)) {
if (!first)
fmtBuf.append('0');
first = false;
}
}
}
fmtBuf.append('E');
placeZeros(fmtBuf, exponentSpecials.subList(2, exponentSpecials.size()));
decimalFmt = new DecimalFormat(fmtBuf.toString(), getDecimalFormatSymbols());
printfFmt = null;
}
desc = descBuf.toString();
}
private DecimalFormatSymbols getDecimalFormatSymbols() {
return DecimalFormatSymbols.getInstance(locale);
}
private static void placeZeros(StringBuffer sb, List<Special> specials) {
for (Special s : specials) {
if (isDigitFmt(s)) {
sb.append('0');
}
}
}
private static CellNumberStringMod insertMod(Special special, CharSequence toAdd, int where) {
return new CellNumberStringMod(special, toAdd, where);
}
private static CellNumberStringMod deleteMod(Special start, boolean startInclusive, Special end, boolean endInclusive) {
return new CellNumberStringMod(start, startInclusive, end, endInclusive);
}
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<Special> numSpecials) {
return "%0" + numSpecials.size() + "d";
}
private static int maxValue(List<Special> s) {
return Math.toIntExact(Math.round(Math.pow(10, Math.min(s.size(), SimpleFraction.MAX_DENOMINATOR_DIGITS)) - 1));
}
private List<Special> specialsFor(int pos, int takeFirst) {
if (pos >= specials.size()) {
return Collections.emptyList();
}
ListIterator<Special> it = specials.listIterator(pos + takeFirst);
Special last = it.next();
int end = pos + takeFirst;
while (it.hasNext()) {
Special s = it.next();
if (!isDigitFmt(s) || s.pos - last.pos > 1)
break;
end++;
last = s;
}
return specials.subList(pos, end + 1);
}
private List<Special> specialsFor(int pos) {
return specialsFor(pos, 0);
}
private static boolean isDigitFmt(Special s) {
return s.ch == '0' || s.ch == '?' || s.ch == '#';
}
private int calculateIntegerPartWidth() {
int digitCount = 0;
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) {
break;
} else if (isDigitFmt(s)) {
digitCount++;
}
}
return digitCount;
}
private static int interpretPrecision(Special decimalPoint, List<Special> specials) {
int idx = specials.indexOf(decimalPoint);
int precision = 0;
if (idx != -1) {
// skip over the decimal point itself
ListIterator<Special> it = specials.listIterator(idx+1);
while (it.hasNext()) {
Special s = it.next();
if (!isDigitFmt(s)) {
break;
}
precision++;
}
}
return precision;
}
private static boolean interpretIntegerCommas
(StringBuffer sb, List<Special> 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<Special> it = specials.listIterator(integerEnd);
boolean stillScaling = true;
boolean integerCommas = false;
while (it.hasPrevious()) {
Special s = it.previous();
if (s.ch != ',') {
stillScaling = false;
} else {
if (stillScaling) {
scale[0] /= 1000;
} else {
integerCommas = true;
}
}
}
if (decimalPoint != null) {
it = specials.listIterator(fractionalEnd);
while (it.hasPrevious()) {
Special s = it.previous();
if (s.ch != ',') {
break;
} else {
scale[0] /= 1000;
}
}
}
// Now strip them out -- we only need their interpretation, not their presence
it = specials.listIterator();
int removed = 0;
while (it.hasNext()) {
Special s = it.next();
s.pos -= removed;
if (s.ch == ',') {
removed++;
it.remove();
sb.deleteCharAt(s.pos);
}
}
return integerCommas;
}
private int integerEnd() {
return (afterInteger == null) ? specials.size() : specials.indexOf(afterInteger);
}
private int fractionalEnd() {
return (afterFractional == null) ? specials.size() : specials.indexOf(afterFractional);
}
@Override
public void formatValue(StringBuffer toAppendTo, Object valueObject) {
if (!(valueObject instanceof Number)) {
throw new IllegalArgumentException("Expected object of type number for format " + format + ", but had: " + valueObject);
}
BigDecimal bd = BigDecimal.valueOf(((Number) valueObject).doubleValue()).multiply(BigDecimal.valueOf(scale));
double value = bd.doubleValue();
// For negative numbers:
// - If the cell format has a negative number format, this method
// is called with a positive value and the number format has
// the negative formatting required, e.g. minus sign or brackets.
// - If the cell format does not have a negative number format,
// this method is called with a negative value and the number is
// formatted with a minus sign at the start.
boolean negative = value < 0;
if (negative)
value = -value;
// Split out the fractional part if we need to print a fraction
double fractional = 0;
if (slash != null) {
if (improperFraction) {
fractional = value;
value = 0;
} else {
fractional = value % 1.0;
// not a cast to long, which shows a value above Long.MAX_VALUE as 9223372036854775807
value = Math.floor(value);
}
}
Set<CellNumberStringMod> mods = new TreeSet<>();
StringBuffer output = new StringBuffer(localiseFormat(desc));
if (exponent != null) {
writeScientific(value, output, mods);
} else if (improperFraction) {
writeFraction(value, null, fractional, output, mods);
} else {
StringBuffer result = new StringBuffer();
try (Formatter f = new Formatter(result, locale)) {
f.format(locale, printfFmt, value);
} catch (IllegalFormatException e) {
throw new IllegalArgumentException("Format: " + printfFmt, e);
}
if (numerator == null) {
writeFractional(result, output);
writeInteger(result, output, integerSpecials, mods, showGroupingSeparator);
} else {
writeFraction(value, result, fractional, output, mods);
}
}
DecimalFormatSymbols dfs = getDecimalFormatSymbols();
String groupingSeparator = Character.toString(dfs.getGroupingSeparator());
// Now strip out any remaining '#'s and add any pending text ...
Iterator<CellNumberStringMod> changes = mods.iterator();
CellNumberStringMod nextChange = (changes.hasNext() ? changes.next() : null);
// records chars already deleted
SparseBitSet deletedChars = new SparseBitSet();
int adjust = 0;
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.getSpecial()) {
int lenBefore = output.length();
int modPos = s.pos + adjust;
switch (nextChange.getOp()) {
case CellNumberStringMod.AFTER:
// ignore adding a comma after a deleted char (which was a '#')
if (nextChange.getToAdd().equals(groupingSeparator) && deletedChars.get(s.pos)) {
break;
}
output.insert(modPos + 1, nextChange.getToAdd());
break;
case CellNumberStringMod.BEFORE:
output.insert(modPos, nextChange.getToAdd());
break;
case CellNumberStringMod.REPLACE:
// delete starting pos in original coordinates
int delPos = s.pos;
if (!nextChange.isStartInclusive()) {
delPos++;
modPos++;
}
// Skip over anything already deleted
while (deletedChars.get(delPos)) {
delPos++;
modPos++;
}
if (nextChange.getEnd() == null) {
throw new IllegalArgumentException("Cannot apply format, end could not determined for format: " + format);
}
// delete end point in original
int delEndPos = nextChange.getEnd().pos;
if (nextChange.isEndInclusive()) {
delEndPos++;
}
// delete end point in current
int modEndPos = delEndPos + adjust;
if (modPos < modEndPos) {
if (nextChange.getToAdd() != null && nextChange.getToAdd().length() == 0) {
output.delete(modPos, modEndPos);
}
else {
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.getOp());
}
adjust += output.length() - lenBefore;
nextChange = (changes.hasNext()) ? changes.next() : null;
}
}
// Finally, add it to the string
if (negative) {
toAppendTo.append('-');
}
toAppendTo.append(output);
}
private void writeScientific(double value, StringBuffer output, Set<CellNumberStringMod> mods) {
StringBuffer result = new StringBuffer();
FieldPosition fractionPos = new FieldPosition(NumberFormat.FRACTION_FIELD);
decimalFmt.format(value, result, fractionPos);
writeInteger(result, output, integerSpecials, mods, showGroupingSeparator);
writeFractional(result, output);
/*
* Exponent sign handling is complex.
*
* In DecimalFormat, you never put the sign in the format, and the sign only
* comes out of the format if it is negative.
*
* In Excel, you always say whether to always show the sign ("e+") or only
* show negative signs ("e-").
*
* Also in Excel, where you put the sign in the format is NOT where it comes
* out in the result. In the format, the sign goes with the "e"; in the
* output it goes with the exponent value. That is, if you say "#e-|#" you
* get "1e|-5", not "1e-|5". This makes sense I suppose, but it complicates
* things.
*
* Finally, everything else in this formatting code assumes that the base of
* the result is the original format, and that starting from that situation,
* the indexes of the original special characters can be used to place the new
* characters. As just described, this is not true for the exponent's sign.
* <p>
* So here is how we handle it:
*
* (1) When parsing the format, remove the sign from after the 'e' and put it
* before the first digit of the exponent (where it will be shown).
*
* (2) Determine the result's sign.
*
* (3) If it's missing, put the sign into the output to keep the result
* lined up with the output. (In the result, "after the 'e'" and "before the
* first digit" are the same because the result has no extra chars to be in
* the way.)
*
* (4) In the output, remove the sign if it should not be shown ("e-" was used
* and the sign is negative) or set it to the correct value.
*/
// (2) Determine the result's sign.
int ePos = fractionPos.getEndIndex();
int signPos = ePos + 1;
char expSignRes = result.charAt(signPos);
if (expSignRes != '-') {
// not a sign, so it's a digit, and therefore a positive exponent
expSignRes = '+';
// (3) If it's missing, put the sign into the output to keep the result
// lined up with the output.
result.insert(signPos, '+');
}
// Now the result lines up like it is supposed to with the specials' indexes
ListIterator<Special> it = exponentSpecials.listIterator(1);
Special expSign = it.next();
char expSignFmt = expSign.ch;
// (4) In the output, remove the sign if it should not be shown or set it to
// the correct value.
if (expSignRes == '-' || expSignFmt == '+') {
mods.add(replaceMod(expSign, true, expSign, true, expSignRes));
} else {
mods.add(deleteMod(expSign, true, expSign, true));
}
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<CellNumberStringMod> mods) {
// the fraction is worked out with ints, so for an improper fraction of a large value it is worked out
// for the part after the whole number, and the whole number is added to the numerator when it is written
BigInteger wholeNumber = null;
if (improperFraction && fractional * Math.max(fixedDenominator, 1) >= Integer.MAX_VALUE) {
double floor = Math.floor(fractional);
wholeNumber = new BigDecimal(floor).toBigInteger();
fractional -= floor;
}
// Work out the fraction first, as it can round to 0, or up to 1
int n;
int d;
// the "fractional % 1" captures integer values in improper fractions
if (fractional == 0 || (improperFraction && fractional % 1 == 0)) {
// 0 as a fraction is reported by excel as 0/1
n = Math.toIntExact(Math.round(fractional));
d = fixedDenominator > 0 ? fixedDenominator : 1;
if (fixedDenominator > 0) {
n *= d;
}
} else if (fixedDenominator > 0) {
// a fixed denominator is always used, without reducing the fraction (2/4 stays 2/4)
d = fixedDenominator;
n = Math.toIntExact(Math.round(fractional * d));
} else if (improperFraction && wholeNumber == null) {
// Excel stops before an improper fraction's numerator exceeds an unsigned 32-bit int:
// 300000.707106781 is 2435705741/8119, where the next fraction would be 5880013860/19601
double floor = Math.floor(fractional);
wholeNumber = BigInteger.valueOf((long) floor);
SimpleFraction frac = SimpleFraction.buildFractionMaxDenominator(fractional - floor, maxDenominator + 1,
(long) floor, MAX_IMPROPER_NUMERATOR);
n = frac.getNumerator();
d = frac.getDenominator();
} else {
// buildFractionMaxDenominator only gives denominators below the maximum it is passed, so pass
// 10^digits, as FractionFormat does: e.g. "??/??" shows 0.99 as 98/99, where 99 would give 1/1
SimpleFraction frac = SimpleFraction.buildFractionMaxDenominator(fractional, maxDenominator + 1);
n = frac.getNumerator();
d = frac.getDenominator();
}
if (!improperFraction && n >= d && n > 0) {
// the fraction rounds up to a whole number: e.g. "# ?/2" shows 0.9 as 1
value += 1;
n = 0;
result = new StringBuffer();
try (Formatter f = new Formatter(result, locale)) {
f.format(locale, printfFmt, value);
}
}
boolean fractionIsZero = n == 0;
// 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.
if (!improperFraction) {
// If fractional part is zero, and numerator doesn't have '0', write out
// only the integer part and strip the rest.
if (fractionIsZero && !hasChar('0', numeratorSpecials)) {
writeInteger(result, output, integerSpecials, mods, false);
Special start = lastSpecial(integerSpecials);
Special end = denominatorSpecials.isEmpty() ? fixedDenominatorEnd : 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 {
// otherwise, remove the fraction
mods.add(deleteMod(start, false, end, true));
}
// That's all, just return
return;
} else {
// New we check to see if we should remove the integer part
boolean numNoZero = !hasChar('0', numeratorSpecials);
boolean intNoZero = !hasChar('0', integerSpecials);
boolean intOnlyHash = integerSpecials.isEmpty() || (integerSpecials.size() == 1 && hasChar('#', integerSpecials));
boolean removeBecauseZero = fractionIsZero && (intOnlyHash || numNoZero);
boolean removeBecauseFraction = !fractionIsZero && intNoZero;
if (value == 0 && (removeBecauseZero || removeBecauseFraction)) {
Special start = lastSpecial(integerSpecials);
boolean hasPlaceHolder = hasChar('?', integerSpecials, numeratorSpecials);
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);
}
}
}
// Print the actual fraction (improper or otherwise)
try {
if (improperFraction) {
n = Math.toIntExact(n + Math.round(value * d));
}
Number numerator = wholeNumber == null ? (Number) n
: wholeNumber.multiply(BigInteger.valueOf(d)).add(BigInteger.valueOf(n));
writeSingleInteger(numeratorFmt, numerator, output, numeratorSpecials, mods);
if (fixedDenominator == 0) {
// a fixed denominator is already in the output
writeDenominator(d, output, mods);
}
} catch (RuntimeException e) {
LOG.atError().withThrowable(e).log("error while fraction evaluation");
}
}
/**
* Shows the decimal point as the locale's decimal separator. The grouping commas have
* already been removed from the format, so any other '.' or ',' is literal text, which Excel
* shows as it is, e.g. the dot in {@code 0.00"x."}
*/
private String localiseFormat(String format) {
char decimalSeparator = getDecimalFormatSymbols().getDecimalSeparator();
if (decimalPointPos < 0 || decimalPointPos >= format.length() || decimalSeparator == '.'
|| format.charAt(decimalPointPos) != '.') {
return format;
}
return format.substring(0, decimalPointPos) + decimalSeparator + format.substring(decimalPointPos + 1);
}
private static boolean hasChar(char ch, List<Special> numSpecials) {
for (Special s : numSpecials) {
if (s.ch == ch) {
return true;
}
}
return false;
}
private static boolean hasChar(char ch, List<Special> numSpecials1, List<Special> numSpecials2) {
return hasChar(ch, numSpecials1) || hasChar(ch, numSpecials2);
}
private static boolean hasChar(char ch, List<Special> numSpecials1, List<Special> numSpecials2,
List<Special> numSpecials3) {
return hasChar(ch, numSpecials1) || hasChar(ch, numSpecials2) || hasChar(ch, numSpecials3);
}
/**
* Excel aligns the denominator of a fraction to the left, so its '?' placeholders
* give spaces after it: "# ???/???" shows 1.05 as "1 1/20 "
*/
private void writeDenominator(int d, StringBuffer output, Set<CellNumberStringMod> mods) {
String digits = Integer.toString(d);
if (digits.length() >= denominatorSpecials.size() || hasChar('0', denominatorSpecials)) {
writeSingleInteger(denominatorFmt, d, output, denominatorSpecials, mods);
return;
}
for (int i = 0; i < denominatorSpecials.size(); i++) {
Special s = denominatorSpecials.get(i);
if (i < digits.length()) {
output.setCharAt(s.pos, digits.charAt(i));
} else if (s.ch == '?') {
output.setCharAt(s.pos, ' ');
}
// an unused '#' is removed later
}
}
private void writeSingleInteger(String fmt, Number num, StringBuffer output, List<Special> numSpecials, Set<CellNumberStringMod> mods) {
StringBuffer sb = new StringBuffer();
try (Formatter formatter = new Formatter(sb, locale)) {
formatter.format(locale, fmt, num);
}
writeInteger(sb, output, numSpecials, mods, false);
}
private void writeInteger(StringBuffer result, StringBuffer output,
List<Special> numSpecials, Set<CellNumberStringMod> mods,
boolean showGroupingSeparator) {
DecimalFormatSymbols dfs = getDecimalFormatSymbols();
String decimalSeparator = Character.toString(dfs.getDecimalSeparator());
String groupingSeparator = Character.toString(dfs.getGroupingSeparator());
int pos = result.indexOf(decimalSeparator) - 1;
if (pos < 0) {
if (exponent != null && numSpecials == integerSpecials) {
pos = result.indexOf("E") - 1;
} else {
pos = result.length() - 1;
}
}
int strip;
for (strip = 0; strip < pos; strip++) {
char resultCh = result.charAt(strip);
if (resultCh != '0' && resultCh != dfs.getGroupingSeparator()) {
break;
}
}
// an integer part of zero is insignificant too: only a '0' placeholder shows it,
// e.g. "??" shows 0 as two spaces, and "#.00" shows 0.5 as ".50"
// (but not in fractions or scientific notation, where Excel shows the 0)
if (exponent == null && slash == null && strip == pos && pos >= 0 && result.charAt(pos) == '0') {
strip++;
}
ListIterator<Special> it = numSpecials.listIterator(numSpecials.size());
Special lastOutputIntegerDigit = null;
int digit = 0;
while (it.hasPrevious()) {
char resultCh;
if (pos >= 0) {
resultCh = result.charAt(pos);
} else {
// If result is shorter than field, pretend there are leading zeros
resultCh = '0';
}
Special s = it.previous();
boolean followWithGroupingSeparator = showGroupingSeparator && digit > 0 && digit % 3 == 0;
boolean zeroStrip = false;
if (resultCh != '0' || s.ch == '0' || s.ch == '?' || pos >= strip) {
zeroStrip = s.ch == '?' && pos < strip;
output.setCharAt(s.pos, (zeroStrip ? ' ' : resultCh));
lastOutputIntegerDigit = s;
}
if (followWithGroupingSeparator) {
mods.add(insertMod(s, zeroStrip ? " " : groupingSeparator, CellNumberStringMod.AFTER));
}
digit++;
--pos;
}
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 was decremented at the end of the loop above when the iterator was at its end
++pos;
StringBuffer extraLeadingDigits = new StringBuffer(result.substring(0, pos));
if (showGroupingSeparator) {
while (pos > 0) {
if (digit > 0 && digit % 3 == 0) {
extraLeadingDigits.insert(pos, groupingSeparator);
}
digit++;
--pos;
}
}
mods.add(insertMod(lastOutputIntegerDigit, extraLeadingDigits, CellNumberStringMod.BEFORE));
}
}
private void writeFractional(StringBuffer result, StringBuffer output) {
int digit;
int strip;
if (!fractionalSpecials.isEmpty()) {
String decimalSeparator = Character.toString(getDecimalFormatSymbols().getDecimalSeparator());
digit = result.indexOf(decimalSeparator) + 1;
if (exponent != null) {
strip = result.indexOf("e") - 1;
} else {
strip = result.length() - 1;
}
while (strip > digit && result.charAt(strip) == '0') {
strip--;
}
for (Special s : fractionalSpecials) {
char resultCh = result.charAt(digit);
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
output.setCharAt(s.pos, ' ');
}
digit++;
}
}
}
/**
* {@inheritDoc}
* <p>
* For a number, this is {@code "#"} for integer values, and {@code "#.#"}
* for floating-point values.
*/
@Override
public void simpleValue(StringBuffer toAppendTo, Object value) {
SIMPLE_NUMBER.formatValue(toAppendTo, value);
}
private static Special lastSpecial(List<Special> s) {
return s.get(s.size() - 1);
}
}