FractionFormat.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.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 = Math.min(len, MAX_DENOM_POW);
                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 double d = num.doubleValue();
        return format(new BigDecimal(d));
    }

    @SuppressWarnings("squid:S2111")
    private String format(final BigDecimal decimal) {
        final boolean isNeg = decimal.compareTo(BigDecimal.ZERO) < 0;

        final BigDecimal absValue = decimal.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(decimal.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(BigDecimal.valueOf(fden)).add(BigDecimal.valueOf(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();
    }

    @Override
    public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
        if (!(obj instanceof Number)) {
            throw new IllegalArgumentException("Cannot format object of " + obj.getClass() + " to number: " + obj);
        }

        return toAppendTo.append(format((Number)obj));
    }

    @Override
    public Object parseObject(String source, ParsePosition pos) {
        throw new NotImplementedException("Reverse parsing not supported");
    }

}