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 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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.text.FieldPosition;
  19. import java.text.Format;
  20. import java.text.ParsePosition;
  21. import java.util.regex.Matcher;
  22. import java.util.regex.Pattern;
  23. import org.apache.poi.ss.formula.eval.NotImplementedException;
  24. /**
  25. * <p>Format class that handles Excel style fractions, such as "# #/#" and "#/###"</p>
  26. *
  27. * <p>As of this writing, this is still not 100% accurate, but it does a reasonable job
  28. * of trying to mimic Excel's fraction calculations. It does not currently
  29. * maintain Excel's spacing.</p>
  30. *
  31. * <p>This class relies on a method lifted nearly verbatim from org.apache.math.fraction.
  32. * If further uses for Commons Math are found, we will consider adding it as a dependency.
  33. * For now, we have in-lined the one method to keep things simple.</p>
  34. */
  35. /* One question remains...is the value of epsilon in calcFractionMaxDenom reasonable? */
  36. @SuppressWarnings("serial")
  37. public class FractionFormat extends Format {
  38. private final static Pattern DENOM_FORMAT_PATTERN = Pattern.compile("(?:(#+)|(\\d+))");
  39. //this was chosen to match the earlier limitation of max denom power
  40. //it can be expanded to get closer to Excel's calculations
  41. //with custom formats # #/#########
  42. //but as of this writing, the numerators and denominators
  43. //with formats of that nature on very small values were quite
  44. //far from Excel's calculations
  45. private final static int MAX_DENOM_POW = 4;
  46. //there are two options:
  47. //a) an exact denominator is specified in the formatString
  48. //b) the maximum denominator can be calculated from the formatString
  49. private final int exactDenom;
  50. private final int maxDenom;
  51. private final String wholePartFormatString;
  52. /**
  53. * Single parameter ctor
  54. * @param denomFormatString The format string for the denominator
  55. */
  56. public FractionFormat(String wholePartFormatString, String denomFormatString) {
  57. this.wholePartFormatString = wholePartFormatString;
  58. //init exactDenom and maxDenom
  59. Matcher m = DENOM_FORMAT_PATTERN.matcher(denomFormatString);
  60. int tmpExact = -1;
  61. int tmpMax = -1;
  62. if (m.find()){
  63. if (m.group(2) != null){
  64. try{
  65. tmpExact = Integer.parseInt(m.group(2));
  66. //if the denom is 0, fall back to the default: tmpExact=100
  67. if (tmpExact == 0){
  68. tmpExact = -1;
  69. }
  70. } catch (NumberFormatException e){
  71. //should never happen
  72. }
  73. } else if (m.group(1) != null) {
  74. int len = m.group(1).length();
  75. len = len > MAX_DENOM_POW ? MAX_DENOM_POW : len;
  76. tmpMax = (int)Math.pow(10, len);
  77. } else {
  78. tmpExact = 100;
  79. }
  80. }
  81. if (tmpExact <= 0 && tmpMax <= 0){
  82. //use 100 as the default denom if something went horribly wrong
  83. tmpExact = 100;
  84. }
  85. exactDenom = tmpExact;
  86. maxDenom = tmpMax;
  87. }
  88. public String format(Number num) {
  89. double doubleValue = num.doubleValue();
  90. boolean isNeg = (doubleValue < 0.0f) ? true : false;
  91. double absDoubleValue = Math.abs(doubleValue);
  92. double wholePart = Math.floor(absDoubleValue);
  93. double decPart = absDoubleValue - wholePart;
  94. if (wholePart + decPart == 0) {
  95. return "0";
  96. }
  97. //if the absolute value is smaller than 1 over the exact or maxDenom
  98. //you can stop here and return "0"
  99. if (absDoubleValue < (1/Math.max(exactDenom, maxDenom))){
  100. return "0";
  101. }
  102. //this is necessary to prevent overflow in the maxDenom calculation
  103. //stink1
  104. if (wholePart+(int)decPart == wholePart+decPart){
  105. StringBuilder sb = new StringBuilder();
  106. if (isNeg){
  107. sb.append("-");
  108. }
  109. sb.append(Integer.toString((int)wholePart));
  110. return sb.toString();
  111. }
  112. SimpleFraction fract = null;
  113. try{
  114. //this should be the case because of the constructor
  115. if (exactDenom > 0){
  116. fract = calcFractionExactDenom(decPart, exactDenom);
  117. } else {
  118. fract = calcFractionMaxDenom(decPart, maxDenom);
  119. }
  120. } catch (SimpleFractionException e){
  121. e.printStackTrace();
  122. return Double.toString(doubleValue);
  123. }
  124. StringBuilder sb = new StringBuilder();
  125. //now format the results
  126. if (isNeg){
  127. sb.append("-");
  128. }
  129. //if whole part has to go into the numerator
  130. if ("".equals(wholePartFormatString)){
  131. int trueNum = (fract.getDenominator()*(int)wholePart)+fract.getNumerator();
  132. sb.append(trueNum).append("/").append(fract.getDenominator());
  133. return sb.toString();
  134. }
  135. //short circuit if fraction is 0 or 1
  136. if (fract.getNumerator() == 0){
  137. sb.append(Integer.toString((int)wholePart));
  138. return sb.toString();
  139. } else if (fract.getNumerator() == fract.getDenominator()){
  140. sb.append(Integer.toString((int)wholePart+1));
  141. return sb.toString();
  142. }
  143. //as mentioned above, this ignores the exact space formatting in Excel
  144. if (wholePart > 0){
  145. sb.append(Integer.toString((int)wholePart)).append(" ");
  146. }
  147. sb.append(fract.getNumerator()).append("/").append(fract.getDenominator());
  148. return sb.toString();
  149. }
  150. public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
  151. return toAppendTo.append(format((Number)obj));
  152. }
  153. public Object parseObject(String source, ParsePosition pos) {
  154. throw new NotImplementedException("Reverse parsing not supported");
  155. }
  156. private SimpleFraction calcFractionMaxDenom(double value, int maxDenominator)
  157. throws SimpleFractionException{
  158. /*
  159. * Lifted wholesale from org.apache.math.fraction.Fraction 2.2
  160. */
  161. double epsilon = 0.000000000001f;
  162. int maxIterations = 100;
  163. long overflow = Integer.MAX_VALUE;
  164. double r0 = value;
  165. long a0 = (long)Math.floor(r0);
  166. if (Math.abs(a0) > overflow) {
  167. throw new SimpleFractionException(
  168. String.format("value > Integer.MAX_VALUE: %d.", a0));
  169. }
  170. // check for (almost) integer arguments, which should not go
  171. // to iterations.
  172. if (Math.abs(a0 - value) < epsilon) {
  173. return new SimpleFraction((int) a0, 1);
  174. }
  175. long p0 = 1;
  176. long q0 = 0;
  177. long p1 = a0;
  178. long q1 = 1;
  179. long p2 = 0;
  180. long q2 = 1;
  181. int n = 0;
  182. boolean stop = false;
  183. do {
  184. ++n;
  185. double r1 = 1.0 / (r0 - a0);
  186. long a1 = (long)Math.floor(r1);
  187. p2 = (a1 * p1) + p0;
  188. q2 = (a1 * q1) + q0;
  189. if ((Math.abs(p2) > overflow) || (Math.abs(q2) > overflow)) {
  190. throw new SimpleFractionException(
  191. String.format("Greater than overflow in loop %f, %d, %d", value, p2, q2));
  192. }
  193. double convergent = (double)p2 / (double)q2;
  194. if (n < maxIterations && Math.abs(convergent - value) > epsilon && q2 < maxDenominator) {
  195. p0 = p1;
  196. p1 = p2;
  197. q0 = q1;
  198. q1 = q2;
  199. a0 = a1;
  200. r0 = r1;
  201. } else {
  202. stop = true;
  203. }
  204. } while (!stop);
  205. if (n >= maxIterations) {
  206. throw new SimpleFractionException("n greater than max iterations " + value + " : " + maxIterations);
  207. }
  208. if (q2 < maxDenominator) {
  209. return new SimpleFraction((int) p2, (int) q2);
  210. } else {
  211. return new SimpleFraction((int) p1, (int) q1);
  212. }
  213. }
  214. private SimpleFraction calcFractionExactDenom(double val, int exactDenom){
  215. int num = (int)Math.round(val*(double)exactDenom);
  216. return new SimpleFraction(num,exactDenom);
  217. }
  218. private class SimpleFraction {
  219. private final int num;
  220. private final int denom;
  221. public SimpleFraction(int num, int denom) {
  222. this.num = num;
  223. this.denom = denom;
  224. }
  225. public int getNumerator() {
  226. return num;
  227. }
  228. public int getDenominator() {
  229. return denom;
  230. }
  231. }
  232. private class SimpleFractionException extends Throwable{
  233. private SimpleFractionException(String message){
  234. super(message);
  235. }
  236. }
  237. }