blob: c89838a5da6feda6c723ec052bafada8b9275a20 [file] [log] [blame]
/*
* 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.usermodel;
import java.math.BigDecimal;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.ss.format.SimpleFraction;
import org.apache.poi.ss.formula.eval.NotImplementedException;
/**
* <p>Format class that handles Excel style fractions, such as "# #/#" and "#/###"</p>
*
* <p>As of this writing, this is still not 100% accurate, but it does a reasonable job
* of trying to mimic Excel's fraction calculations. It does not currently
* maintain Excel's spacing.</p>
*
* <p>This class relies on a method lifted nearly verbatim from org.apache.math.fraction.
* If further uses for Commons Math are found, we will consider adding it as a dependency.
* For now, we have in-lined the one method to keep things simple.</p>
*/
@SuppressWarnings("serial")
public class FractionFormat extends Format {
private static final Logger LOGGER = LogManager.getLogger(FractionFormat.class);
private static final Pattern DENOM_FORMAT_PATTERN = Pattern.compile("(?:(#+)|(\\d+))");
//this was chosen to match the earlier limitation of max denom power
//it can be expanded to get closer to Excel's calculations
//with custom formats # #/#########
//but as of this writing, the numerators and denominators
//with formats of that nature on very small values were quite
//far from Excel's calculations
private static final int MAX_DENOM_POW = 4;
//there are two options:
//a) an exact denominator is specified in the formatString
//b) the maximum denominator can be calculated from the formatString
private final int exactDenom;
private final int maxDenom;
private final String wholePartFormatString;
/**
* Single parameter ctor
* @param denomFormatString The format string for the denominator
*/
public FractionFormat(String wholePartFormatString, String denomFormatString) {
this.wholePartFormatString = wholePartFormatString;
// initialize exactDenom and maxDenom
Matcher m = DENOM_FORMAT_PATTERN.matcher(denomFormatString);
int tmpExact = -1;
int tmpMax = -1;
if (m.find()){
if (m.group(2) != null){
try{
tmpExact = Integer.parseInt(m.group(2));
//if the denom is 0, fall back to the default: tmpExact=100
if (tmpExact == 0){
tmpExact = -1;
}
} catch (NumberFormatException e){
// should not happen because the pattern already verifies that this is a number,
// but a number larger than Integer.MAX_VALUE can cause it,
// so throw an exception if we somehow end up here
throw new IllegalStateException(e);
}
} else if (m.group(1) != null) {
int len = m.group(1).length();
len = len > MAX_DENOM_POW ? MAX_DENOM_POW : len;
tmpMax = (int)Math.pow(10, len);
} else {
tmpExact = 100;
}
}
if (tmpExact <= 0 && tmpMax <= 0){
//use 100 as the default denom if something went horribly wrong
tmpExact = 100;
}
exactDenom = tmpExact;
maxDenom = tmpMax;
}
@SuppressWarnings("squid:S2111")
public String format(Number num) {
final BigDecimal doubleValue = new BigDecimal(num.doubleValue());
final boolean isNeg = doubleValue.compareTo(BigDecimal.ZERO) < 0;
final BigDecimal absValue = doubleValue.abs();
final BigDecimal wholePart = new BigDecimal(absValue.toBigInteger());
final BigDecimal decPart = absValue.remainder(BigDecimal.ONE);
if (wholePart.add(decPart).compareTo(BigDecimal.ZERO) == 0) {
return "0";
}
// if the absolute value is smaller than 1 over the exact or maxDenom
// you can stop here and return "0"
// reciprocal is result of an int devision ... and so it's nearly always 0
// double reciprocal = 1/Math.max(exactDenom, maxDenom);
// if (absDoubleValue < reciprocal) {
// return "0";
// }
//this is necessary to prevent overflow in the maxDenom calculation
if (decPart.compareTo(BigDecimal.ZERO) == 0){
StringBuilder sb = new StringBuilder();
if (isNeg){
sb.append("-");
}
sb.append(wholePart);
return sb.toString();
}
final SimpleFraction fract;
try {
//this should be the case because of the constructor
if (exactDenom > 0){
fract = SimpleFraction.buildFractionExactDenominator(decPart.doubleValue(), exactDenom);
} else {
fract = SimpleFraction.buildFractionMaxDenominator(decPart.doubleValue(), maxDenom);
}
} catch (RuntimeException e){
LOGGER.atWarn().withThrowable(e).log("Can't format fraction");
return Double.toString(doubleValue.doubleValue());
}
StringBuilder sb = new StringBuilder();
//now format the results
if (isNeg){
sb.append("-");
}
//if whole part has to go into the numerator
if (wholePartFormatString == null || wholePartFormatString.isEmpty()){
final int fden = fract.getDenominator();
final int fnum = fract.getNumerator();
BigDecimal trueNum = wholePart.multiply(new BigDecimal(fden)).add(new BigDecimal(fnum));
sb.append(trueNum.toBigInteger()).append("/").append(fden);
return sb.toString();
}
//short circuit if fraction is 0 or 1
if (fract.getNumerator() == 0){
sb.append(wholePart);
return sb.toString();
} else if (fract.getNumerator() == fract.getDenominator()){
sb.append(wholePart.add(BigDecimal.ONE));
return sb.toString();
}
//as mentioned above, this ignores the exact space formatting in Excel
if (wholePart.compareTo(BigDecimal.ZERO) > 0){
sb.append(wholePart).append(" ");
}
sb.append(fract.getNumerator()).append("/").append(fract.getDenominator());
return sb.toString();
}
public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
return toAppendTo.append(format((Number)obj));
}
public Object parseObject(String source, ParsePosition pos) {
throw new NotImplementedException("Reverse parsing not supported");
}
}