org.apache.poi.ss.usermodel.FractionFormat Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of apache-poi Show documentation
Show all versions of apache-poi Show documentation
The Apache Commons Codec package contains simple encoder and decoders for
various formats such as Base64 and Hexadecimal. In addition to these
widely used encoders and decoders, the codec package also maintains a
collection of phonetic encoding utilities.
/*
* 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;
/**
* Format class that handles Excel style fractions, such as "# #/#" and "#/###"
*
* 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.
*
* 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.
*/
@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");
}
}