View Javadoc
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 }