1 package nom.tam.util; 2 3 /* 4 * #%L 5 * nom.tam FITS library 6 * %% 7 * Copyright (C) 1996 - 2024 nom-tam-fits 8 * %% 9 * This is free and unencumbered software released into the public domain. 10 * 11 * Anyone is free to copy, modify, publish, use, compile, sell, or 12 * distribute this software, either in source code form or as a compiled 13 * binary, for any purpose, commercial or non-commercial, and by any 14 * means. 15 * 16 * In jurisdictions that recognize copyright laws, the author or authors 17 * of this software dedicate any and all copyright interest in the 18 * software to the public domain. We make this dedication for the benefit 19 * of the public at large and to the detriment of our heirs and 20 * successors. We intend this dedication to be an overt act of 21 * relinquishment in perpetuity of all present and future rights to this 22 * software under copyright law. 23 * 24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 25 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 27 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 28 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 29 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 30 * OTHER DEALINGS IN THE SOFTWARE. 31 * #L% 32 */ 33 34 import java.math.BigDecimal; 35 import java.math.BigInteger; 36 import java.math.RoundingMode; 37 import java.text.DecimalFormat; 38 import java.text.DecimalFormatSymbols; 39 import java.util.Locale; 40 41 import nom.tam.fits.FitsFactory; 42 import nom.tam.fits.HeaderCard; 43 import nom.tam.fits.LongValueException; 44 45 /** 46 * Formatting number values for use in FITS headers. 47 * 48 * @author Attila Kovacs 49 * @since 1.16 50 */ 51 public class FlexFormat { 52 53 /** 54 * Constant to specify the precision (number of decimal places shown) should 55 * be the natural precision of the number type, or reduced at most to 56 * {@link #DOUBLE_DECIMALS} as necessary to fit in the alotted space. 57 */ 58 public static final int AUTO_PRECISION = -1; 59 60 /** 61 * The maximum number of decimal places to show (after the leading figure) 62 * for double-precision (64-bit) values. 63 */ 64 public static final int DOUBLE_DECIMALS = 16; 65 66 /** 67 * The maximum number of decimal places to show (after the leading figure) 68 * for single-precision (32-bit) values. 69 */ 70 public static final int FLOAT_DECIMALS = 7; 71 72 /** 73 * The minimum number of decimal places to show (after the leading figure) 74 * for big-decimal values. 64-bit longs are in the +-1E19 range, so they 75 * provide 18 decimals after the leading figure. We want big integer to 76 * provideat least as many decimal places as a long, when in exponential 77 * form... 78 */ 79 public static final int MIN_BIGINT_EFORM_DECIMALS = 18; 80 81 /** 82 * The exclusive upper limit floating point value that can be shown in fixed 83 * format. Values larger or equals to it will always be shown in exponential 84 * format. This is juist for human readability. If there are more than 5 85 * figures in front of the decimal place, they become harder to comprehend 86 * at first sight than the explicit powers of 10 of the exponential format. 87 */ 88 private static final double MAX_FIXED = 1e6; 89 90 /** 91 * The smallest floating point value that can be shown in fixed format. 92 * Values smallert than this value will always be printed in exponential 93 * format. This is juist for human readability. If there are more than 2 94 * leading zeroes in front of the decimal place, they become harder to 95 * comprehend at first sight than the explicit powers of 10 of the 96 * exponential format. 97 */ 98 private static final double MIN_FIXED = 0.001; 99 100 /** 101 * The maximum number of decimal places to show after the leading figure 102 * (i.e. fractional digits in exponential format). If the value has more 103 * precision than this value it will be rounded to the specified decimal 104 * place. The special value {@link #AUTO_PRECISION} can be used to display 105 * as many of the available decimal places as can fit into the space that is 106 * available (see {@link #setWidth(int)}. 107 */ 108 private int decimals = AUTO_PRECISION; 109 110 /** 111 * The maximum number of characters available for showing number values. 112 * This class will always return numbers that fit in that space, or else 113 * throw an exception. 114 */ 115 private int width = HeaderCard.FITS_HEADER_CARD_SIZE; 116 117 private static final DecimalFormatSymbols SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US); 118 119 /** 120 * Sets the maximum number of decimal places to show after the leading 121 * figure (i.e. fractional digits in exponential format). If the value has 122 * more precision than this value it will be rounded to the specified 123 * decimal place. The special value {@link #AUTO_PRECISION} can be used to 124 * display as many of the available decimal places as can fit into the space 125 * that is available (see {@link #setWidth(int)}. 126 * 127 * @param nDecimals 128 * the requested new number of decimal places to show after the 129 * leading figure, or {@link #AUTO_PRECISION}. If an explicit 130 * value is set, all decimal values will be printed in 131 * exponential format with up to that many fractional digits 132 * showing before the exponent symbol. 133 * @return itself 134 * @see #autoPrecision() 135 * @see #getPrecision() 136 * @see #setWidth(int) 137 * @see #format(Number) 138 */ 139 public synchronized FlexFormat setPrecision(int nDecimals) { 140 decimals = nDecimals < 0 ? AUTO_PRECISION : nDecimals; 141 return this; 142 } 143 144 /** 145 * Selects flexible precision formatting of floating point values. The 146 * values will be printed either in fixed format or exponential format, with 147 * up to the number of decimal places supported by the underlying value. For 148 * {@link BigDecimal} and {@link BigInteger} types, the precision may be 149 * reduced at most down to {@link #DOUBLE_DECIMALS} to make it fit in the 150 * available space. 151 * 152 * @return itself 153 * @see #setPrecision(int) 154 * @see #getPrecision() 155 * @see #setWidth(int) 156 * @see #format(Number) 157 */ 158 public synchronized FlexFormat autoPrecision() { 159 return setPrecision(AUTO_PRECISION); 160 } 161 162 /** 163 * Returns the maximum number of decimal places that will be shown when 164 * formatting floating point values in exponential form, or 165 * {@link #AUTO_PRECISION} if either fixed or exponential form may be used 166 * with up to the native precision of the value, or whatever precision can 167 * be shown in the space available. 168 * 169 * @return the maximum number of decimal places that will be shown when 170 * formatting floating point values, or {@link #AUTO_PRECISION}. 171 * @see #setPrecision(int) 172 * @see #autoPrecision() 173 * @see #setWidth(int) 174 */ 175 public final synchronized int getPrecision() { 176 return decimals; 177 } 178 179 /** 180 * Sets the number of characters that this formatter can use to print number 181 * values. Subsequent calls to {@link #format(Number)} will guarantee to 182 * return only values that are shorter or equals to the specified width, or 183 * else throw an exception. 184 * 185 * @param nChars 186 * the new maximum length for formatted values. 187 * @return itself 188 * @see #getWidth() 189 * @see #forCard(HeaderCard) 190 * @see #setPrecision(int) 191 * @see #format(Number) 192 */ 193 public synchronized FlexFormat setWidth(int nChars) { 194 width = nChars > 0 ? nChars : 0; 195 return this; 196 } 197 198 /** 199 * Sets the number of characters that this formatter can use to print number 200 * values to the space available for the value field in the specified header 201 * card. It is essentially a shorthand for 202 * <code>setWidth(card.spaceForValue())</code>. 203 * 204 * @param card 205 * the header card in which the formatted number values must fit. 206 * @return itself 207 */ 208 public final FlexFormat forCard(HeaderCard card) { 209 return setWidth(card.spaceForValue()); 210 } 211 212 /** 213 * Returns the number of characters that this formatter can use to print 214 * number values 215 * 216 * @return the maximum length for formatted values. 217 */ 218 public final synchronized int getWidth() { 219 return width; 220 } 221 222 /** 223 * Checks if the specified number is a decimal (non-integer) type. 224 * 225 * @param value 226 * the number to check 227 * @return <code>true</code> if the specified number is a decimal type 228 * value, or else <code>false</code> if it is an integer type. 229 */ 230 private static boolean isDecimal(Number value) { 231 return value instanceof Float || value instanceof Double || value instanceof BigDecimal; 232 } 233 234 /** 235 * Returns a string representation of a decimal number, in the available 236 * space, using either fixed decimal format or exponential notitation. It 237 * will use the notation that either gets closer to the required fixed 238 * precision while filling the available space, or if both notations can fit 239 * it will return the more compact one. If neither notation can be 240 * accomodated in the space available, then an exception is thrown. 241 * 242 * @param value 243 * the decimal value to print 244 * @return the string representing the value, or an empty string if the 245 * value was <code>null</code>. 246 * @throws LongValueException 247 * if the decimal value cannot be represented in the alotted 248 * space with any precision 249 * @see #setPrecision(int) 250 * @see #setWidth(int) 251 * @see #forCard(HeaderCard) 252 */ 253 public synchronized String format(Number value) throws LongValueException { 254 255 if (value == null) { 256 return ""; 257 } 258 259 // The value in fixed notation... 260 String fixed = null; 261 262 if (!isDecimal(value)) { 263 // For integer types, always consider the fixed format... 264 fixed = value.toString(); 265 if (fixed.length() <= width) { 266 return fixed; 267 } 268 if (!(value instanceof BigInteger)) { 269 throw new LongValueException(width, fixed); 270 } 271 // We'll try exponential with reduced precision... 272 fixed = null; 273 } else if (decimals < 0) { 274 // Don"t do fixed format if precision is set explicitly 275 // (It's not really trivial to control the number of significant 276 // gigures in the fixed format...) 277 double a = Math.abs(value.doubleValue()); 278 if (a >= MIN_FIXED && a < MAX_FIXED) { 279 // Fixed format only in a resonable data... 280 try { 281 fixed = format(value, "0.#", AUTO_PRECISION, false); 282 } catch (LongValueException e) { 283 // We'll try with exponential notation... 284 } 285 } 286 } 287 288 // The value in exponential notation... 289 String exp = null; 290 291 try { 292 exp = format(value, "0.#E0", decimals, FitsFactory.isUseExponentD()); 293 if (fixed == null) { 294 return exp; 295 } 296 // Go with whichever is more compact. 297 return exp.length() < fixed.length() ? exp : fixed; 298 299 } catch (LongValueException e) { 300 if (fixed == null) { 301 throw e; 302 } 303 } 304 305 return fixed; 306 } 307 308 /** 309 * Returns a fixed decimal representation of a value in the available space. 310 * For BigInteger and BigDecimal types, we allow reducing the precision at 311 * most down to to doube precision, if necessary to fit the number in the 312 * alotted space. If it's not at all possible to fit the fixed 313 * representation in the space available, then an exception is. 314 * 315 * @param value 316 * the decimal value to set 317 * @param fmt 318 * the string that describes the base format (e.g. "0.#" or 319 * "0E0"). 320 * @param nDecimals 321 * the number of decimal places to show 322 * @param allowUseD 323 * if 'D' may be used instead of 'E' to precede the exponent when 324 * value has more than 32-bit floating-point precision. 325 * @return the fixed format decimal representation of the value in the 326 * alotted space. 327 * @throws LongValueException 328 * if the decimal value cannot be represented in the alotted 329 * space with the specified precision 330 */ 331 private synchronized String format(Number value, String fmt, int nDecimals, boolean allowUseD) throws LongValueException { 332 if (width < 1) { 333 throw new LongValueException(width); 334 } 335 336 DecimalFormat f = new DecimalFormat(fmt); 337 f.setDecimalFormatSymbols(SYMBOLS); 338 f.setDecimalSeparatorAlwaysShown(true); 339 f.setRoundingMode(RoundingMode.HALF_UP); 340 341 if (nDecimals < 0) { 342 // Determine precision based on the type. 343 if (value instanceof BigDecimal || value instanceof BigInteger) { 344 nDecimals = width; 345 } else if (value instanceof Double) { 346 nDecimals = DOUBLE_DECIMALS; 347 } else { 348 nDecimals = FLOAT_DECIMALS; 349 } 350 } 351 352 f.setMinimumFractionDigits(fmt.indexOf('E') < 0 ? 1 : 0); 353 f.setMaximumFractionDigits(nDecimals); 354 355 String text = f.format(value); 356 357 // Iterate to make sure we get where we want... 358 while (text.length() > width) { 359 int delta = text.length() - width; 360 nDecimals -= delta; 361 362 if ((value instanceof BigInteger && nDecimals < MIN_BIGINT_EFORM_DECIMALS) || (!(value instanceof BigInteger) && nDecimals < DOUBLE_DECIMALS)) { 363 // We cannot show enough decimals for big types... 364 throw new LongValueException(width, text); 365 } 366 367 f.setMaximumFractionDigits(nDecimals); 368 text = f.format(value); 369 } 370 371 if (allowUseD && nDecimals > FLOAT_DECIMALS) { 372 // If we want 'D' instead of 'E', just replace the letter in the 373 // result. 374 text = text.replace('E', 'D'); 375 } 376 377 return text; 378 } 379 }