You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FractionFormat.java 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package org.apache.poi.ss.usermodel;
  18. import java.math.BigDecimal;
  19. import java.text.FieldPosition;
  20. import java.text.Format;
  21. import java.text.ParsePosition;
  22. import java.util.regex.Matcher;
  23. import java.util.regex.Pattern;
  24. import org.apache.logging.log4j.LogManager;
  25. import org.apache.logging.log4j.Logger;
  26. import org.apache.poi.ss.format.SimpleFraction;
  27. import org.apache.poi.ss.formula.eval.NotImplementedException;
  28. /**
  29. * <p>Format class that handles Excel style fractions, such as "# #/#" and "#/###"</p>
  30. *
  31. * <p>As of this writing, this is still not 100% accurate, but it does a reasonable job
  32. * of trying to mimic Excel's fraction calculations. It does not currently
  33. * maintain Excel's spacing.</p>
  34. *
  35. * <p>This class relies on a method lifted nearly verbatim from org.apache.math.fraction.
  36. * If further uses for Commons Math are found, we will consider adding it as a dependency.
  37. * For now, we have in-lined the one method to keep things simple.</p>
  38. */
  39. @SuppressWarnings("serial")
  40. public class FractionFormat extends Format {
  41. private static final Logger LOGGER = LogManager.getLogger(FractionFormat.class);
  42. private static final Pattern DENOM_FORMAT_PATTERN = Pattern.compile("(?:(#+)|(\\d+))");
  43. //this was chosen to match the earlier limitation of max denom power
  44. //it can be expanded to get closer to Excel's calculations
  45. //with custom formats # #/#########
  46. //but as of this writing, the numerators and denominators
  47. //with formats of that nature on very small values were quite
  48. //far from Excel's calculations
  49. private static final int MAX_DENOM_POW = 4;
  50. //there are two options:
  51. //a) an exact denominator is specified in the formatString
  52. //b) the maximum denominator can be calculated from the formatString
  53. private final int exactDenom;
  54. private final int maxDenom;
  55. private final String wholePartFormatString;
  56. /**
  57. * Single parameter ctor
  58. * @param denomFormatString The format string for the denominator
  59. */
  60. public FractionFormat(String wholePartFormatString, String denomFormatString) {
  61. this.wholePartFormatString = wholePartFormatString;
  62. // initialize exactDenom and maxDenom
  63. Matcher m = DENOM_FORMAT_PATTERN.matcher(denomFormatString);
  64. int tmpExact = -1;
  65. int tmpMax = -1;
  66. if (m.find()){
  67. if (m.group(2) != null){
  68. try{
  69. tmpExact = Integer.parseInt(m.group(2));
  70. //if the denom is 0, fall back to the default: tmpExact=100
  71. if (tmpExact == 0){
  72. tmpExact = -1;
  73. }
  74. } catch (NumberFormatException e){
  75. // should not happen because the pattern already verifies that this is a number,
  76. // but a number larger than Integer.MAX_VALUE can cause it,
  77. // so throw an exception if we somehow end up here
  78. throw new IllegalStateException(e);
  79. }
  80. } else if (m.group(1) != null) {
  81. int len = m.group(1).length();
  82. len = len > MAX_DENOM_POW ? MAX_DENOM_POW : len;
  83. tmpMax = (int)Math.pow(10, len);
  84. } else {
  85. tmpExact = 100;
  86. }
  87. }
  88. if (tmpExact <= 0 && tmpMax <= 0){
  89. //use 100 as the default denom if something went horribly wrong
  90. tmpExact = 100;
  91. }
  92. exactDenom = tmpExact;
  93. maxDenom = tmpMax;
  94. }
  95. @SuppressWarnings("squid:S2111")
  96. public String format(Number num) {
  97. final BigDecimal doubleValue = new BigDecimal(num.doubleValue());
  98. final boolean isNeg = doubleValue.compareTo(BigDecimal.ZERO) < 0;
  99. final BigDecimal absValue = doubleValue.abs();
  100. final BigDecimal wholePart = new BigDecimal(absValue.toBigInteger());
  101. final BigDecimal decPart = absValue.remainder(BigDecimal.ONE);
  102. if (wholePart.add(decPart).compareTo(BigDecimal.ZERO) == 0) {
  103. return "0";
  104. }
  105. // if the absolute value is smaller than 1 over the exact or maxDenom
  106. // you can stop here and return "0"
  107. // reciprocal is result of an int devision ... and so it's nearly always 0
  108. // double reciprocal = 1/Math.max(exactDenom, maxDenom);
  109. // if (absDoubleValue < reciprocal) {
  110. // return "0";
  111. // }
  112. //this is necessary to prevent overflow in the maxDenom calculation
  113. if (decPart.compareTo(BigDecimal.ZERO) == 0){
  114. StringBuilder sb = new StringBuilder();
  115. if (isNeg){
  116. sb.append("-");
  117. }
  118. sb.append(wholePart);
  119. return sb.toString();
  120. }
  121. final SimpleFraction fract;
  122. try {
  123. //this should be the case because of the constructor
  124. if (exactDenom > 0){
  125. fract = SimpleFraction.buildFractionExactDenominator(decPart.doubleValue(), exactDenom);
  126. } else {
  127. fract = SimpleFraction.buildFractionMaxDenominator(decPart.doubleValue(), maxDenom);
  128. }
  129. } catch (RuntimeException e){
  130. LOGGER.atWarn().withThrowable(e).log("Can't format fraction");
  131. return Double.toString(doubleValue.doubleValue());
  132. }
  133. StringBuilder sb = new StringBuilder();
  134. //now format the results
  135. if (isNeg){
  136. sb.append("-");
  137. }
  138. //if whole part has to go into the numerator
  139. if (wholePartFormatString == null || wholePartFormatString.isEmpty()){
  140. final int fden = fract.getDenominator();
  141. final int fnum = fract.getNumerator();
  142. BigDecimal trueNum = wholePart.multiply(new BigDecimal(fden)).add(new BigDecimal(fnum));
  143. sb.append(trueNum.toBigInteger()).append("/").append(fden);
  144. return sb.toString();
  145. }
  146. //short circuit if fraction is 0 or 1
  147. if (fract.getNumerator() == 0){
  148. sb.append(wholePart);
  149. return sb.toString();
  150. } else if (fract.getNumerator() == fract.getDenominator()){
  151. sb.append(wholePart.add(BigDecimal.ONE));
  152. return sb.toString();
  153. }
  154. //as mentioned above, this ignores the exact space formatting in Excel
  155. if (wholePart.compareTo(BigDecimal.ZERO) > 0){
  156. sb.append(wholePart).append(" ");
  157. }
  158. sb.append(fract.getNumerator()).append("/").append(fract.getDenominator());
  159. return sb.toString();
  160. }
  161. public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
  162. return toAppendTo.append(format((Number)obj));
  163. }
  164. public Object parseObject(String source, ParsePosition pos) {
  165. throw new NotImplementedException("Reverse parsing not supported");
  166. }
  167. }