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 /** For thread synchronization */
118 private Object lock = new Object();
119
120 private static final DecimalFormatSymbols SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US);
121
122 /**
123 * Instantiates flexible-width number formatting for numbers in FITS
124 * headers.
125 */
126 public FlexFormat() {
127 }
128
129 /**
130 * Sets the maximum number of decimal places to show after the leading
131 * figure (i.e. fractional digits in exponential format). If the value has
132 * more precision than this value it will be rounded to the specified
133 * decimal place. The special value {@link #AUTO_PRECISION} can be used to
134 * display as many of the available decimal places as can fit into the space
135 * that is available (see {@link #setWidth(int)}.
136 *
137 * @param nDecimals
138 * the requested new number of decimal places to show after the
139 * leading figure, or {@link #AUTO_PRECISION}. If an explicit
140 * value is set, all decimal values will be printed in
141 * exponential format with up to that many fractional digits
142 * showing before the exponent symbol.
143 * @return itself
144 * @see #autoPrecision()
145 * @see #getPrecision()
146 * @see #setWidth(int)
147 * @see #format(Number)
148 */
149 public FlexFormat setPrecision(int nDecimals) {
150 synchronized (lock) {
151 decimals = nDecimals < 0 ? AUTO_PRECISION : nDecimals;
152 }
153 return this;
154 }
155
156 /**
157 * Selects flexible precision formatting of floating point values. The
158 * values will be printed either in fixed format or exponential format, with
159 * up to the number of decimal places supported by the underlying value. For
160 * {@link BigDecimal} and {@link BigInteger} types, the precision may be
161 * reduced at most down to {@link #DOUBLE_DECIMALS} to make it fit in the
162 * available space.
163 *
164 * @return itself
165 * @see #setPrecision(int)
166 * @see #getPrecision()
167 * @see #setWidth(int)
168 * @see #format(Number)
169 */
170 public FlexFormat autoPrecision() {
171 return setPrecision(AUTO_PRECISION);
172 }
173
174 /**
175 * Returns the maximum number of decimal places that will be shown when
176 * formatting floating point values in exponential form, or
177 * {@link #AUTO_PRECISION} if either fixed or exponential form may be used
178 * with up to the native precision of the value, or whatever precision can
179 * be shown in the space available.
180 *
181 * @return the maximum number of decimal places that will be shown when
182 * formatting floating point values, or {@link #AUTO_PRECISION}.
183 * @see #setPrecision(int)
184 * @see #autoPrecision()
185 * @see #setWidth(int)
186 */
187 public final int getPrecision() {
188 synchronized (lock) {
189 return decimals;
190 }
191 }
192
193 /**
194 * Sets the number of characters that this formatter can use to print number
195 * values. Subsequent calls to {@link #format(Number)} will guarantee to
196 * return only values that are shorter or equals to the specified width, or
197 * else throw an exception.
198 *
199 * @param nChars
200 * the new maximum length for formatted values.
201 * @return itself
202 * @see #getWidth()
203 * @see #forCard(HeaderCard)
204 * @see #setPrecision(int)
205 * @see #format(Number)
206 */
207 public FlexFormat setWidth(int nChars) {
208 synchronized (lock) {
209 width = nChars > 0 ? nChars : 0;
210 }
211 return this;
212 }
213
214 /**
215 * Sets the number of characters that this formatter can use to print number
216 * values to the space available for the value field in the specified header
217 * card. It is essentially a shorthand for
218 * <code>setWidth(card.spaceForValue())</code>.
219 *
220 * @param card
221 * the header card in which the formatted number values must fit.
222 * @return itself
223 */
224 public final FlexFormat forCard(HeaderCard card) {
225 return setWidth(card.spaceForValue());
226 }
227
228 /**
229 * Returns the number of characters that this formatter can use to print
230 * number values
231 *
232 * @return the maximum length for formatted values.
233 */
234 public final int getWidth() {
235 synchronized (lock) {
236 return width;
237 }
238 }
239
240 /**
241 * Checks if the specified number is a decimal (non-integer) type.
242 *
243 * @param value
244 * the number to check
245 * @return <code>true</code> if the specified number is a decimal type
246 * value, or else <code>false</code> if it is an integer type.
247 */
248 private static boolean isDecimal(Number value) {
249 return value instanceof Float || value instanceof Double || value instanceof BigDecimal;
250 }
251
252 /**
253 * Returns a string representation of a decimal number, in the available
254 * space, using either fixed decimal format or exponential notitation. It
255 * will use the notation that either gets closer to the required fixed
256 * precision while filling the available space, or if both notations can fit
257 * it will return the more compact one. If neither notation can be
258 * accomodated in the space available, then an exception is thrown.
259 *
260 * @param value
261 * the decimal value to print
262 * @return the string representing the value, or an empty string if the
263 * value was <code>null</code>.
264 * @throws LongValueException
265 * if the decimal value cannot be represented in the alotted
266 * space with any precision
267 * @see #setPrecision(int)
268 * @see #setWidth(int)
269 * @see #forCard(HeaderCard)
270 */
271 public String format(Number value) throws LongValueException {
272
273 if (value == null) {
274 return "";
275 }
276
277 // The value in fixed notation...
278 String fixed = null;
279
280 synchronized (lock) {
281 if (!isDecimal(value)) {
282 // For integer types, always consider the fixed format...
283 fixed = value.toString();
284 if (fixed.length() <= width) {
285 return fixed;
286 }
287 if (!(value instanceof BigInteger)) {
288 throw new LongValueException(width, fixed);
289 }
290 // We'll try exponential with reduced precision...
291 fixed = null;
292 } else if (decimals < 0) {
293 // Don"t do fixed format if precision is set explicitly
294 // (It's not really trivial to control the number of significant
295 // gigures in the fixed format...)
296 double a = Math.abs(value.doubleValue());
297 if (a >= MIN_FIXED && a < MAX_FIXED) {
298 // Fixed format only in a resonable data...
299 try {
300 fixed = format(value, "0.#", AUTO_PRECISION, false);
301 } catch (LongValueException e) {
302 // We'll try with exponential notation...
303 }
304 }
305 }
306
307 // The value in exponential notation...
308 String exp = null;
309
310 try {
311 exp = format(value, "0.#E0", decimals, FitsFactory.isUseExponentD());
312 if (fixed == null) {
313 return exp;
314 }
315 // Go with whichever is more compact.
316 return exp.length() < fixed.length() ? exp : fixed;
317
318 } catch (LongValueException e) {
319 if (fixed == null) {
320 throw e;
321 }
322 }
323 }
324
325 return fixed;
326 }
327
328 /**
329 * Returns a fixed decimal representation of a value in the available space.
330 * For BigInteger and BigDecimal types, we allow reducing the precision at
331 * most down to to doube precision, if necessary to fit the number in the
332 * alotted space. If it's not at all possible to fit the fixed
333 * representation in the space available, then an exception is.
334 *
335 * @param value
336 * the decimal value to set
337 * @param fmt
338 * the string that describes the base format (e.g. "0.#" or
339 * "0E0").
340 * @param nDecimals
341 * the number of decimal places to show
342 * @param allowUseD
343 * if 'D' may be used instead of 'E' to precede the exponent when
344 * value has more than 32-bit floating-point precision.
345 * @return the fixed format decimal representation of the value in the
346 * alotted space.
347 * @throws LongValueException
348 * if the decimal value cannot be represented in the alotted
349 * space with the specified precision
350 */
351 private String format(Number value, String fmt, int nDecimals, boolean allowUseD) throws LongValueException {
352 synchronized (lock) {
353 if (width < 1) {
354 throw new LongValueException(width);
355 }
356
357 DecimalFormat f = new DecimalFormat(fmt);
358 f.setDecimalFormatSymbols(SYMBOLS);
359 f.setDecimalSeparatorAlwaysShown(true);
360 f.setRoundingMode(RoundingMode.HALF_UP);
361
362 if (nDecimals < 0) {
363 // Determine precision based on the type.
364 if (value instanceof BigDecimal || value instanceof BigInteger) {
365 nDecimals = width;
366 } else if (value instanceof Double) {
367 nDecimals = DOUBLE_DECIMALS;
368 } else {
369 nDecimals = FLOAT_DECIMALS;
370 }
371 }
372
373 f.setMinimumFractionDigits(fmt.indexOf('E') < 0 ? 1 : 0);
374 f.setMaximumFractionDigits(nDecimals);
375
376 String text = f.format(value);
377
378 // Iterate to make sure we get where we want...
379 while (text.length() > width) {
380 int delta = text.length() - width;
381 nDecimals -= delta;
382
383 if ((value instanceof BigInteger && nDecimals < MIN_BIGINT_EFORM_DECIMALS) || (!(value instanceof BigInteger) && nDecimals < DOUBLE_DECIMALS)) {
384 // We cannot show enough decimals for big types...
385 throw new LongValueException(width, text);
386 }
387
388 f.setMaximumFractionDigits(nDecimals);
389 text = f.format(value);
390 }
391
392 if (allowUseD && nDecimals > FLOAT_DECIMALS) {
393 // If we want 'D' instead of 'E', just replace the letter in the
394 // result.
395 text = text.replace('E', 'D');
396 }
397
398 return text;
399 }
400 }
401 }