View Javadoc
1   package nom.tam.fits;
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.text.DecimalFormat;
35  import java.util.Calendar;
36  import java.util.Date;
37  import java.util.TimeZone;
38  import java.util.regex.Matcher;
39  import java.util.regex.Pattern;
40  
41  /**
42   * ISO timestamp support for FITS headers. Such timestamps are used with <code>DATE</code> style header keywords, such
43   * as <code>DATE-OBS</code> or <code>DATE-END</code>.
44   */
45  public class FitsDate implements Comparable<FitsDate> {
46  
47      /**
48       * logger to log to.
49       */
50  
51      private static final int FIRST_THREE_CHARACTER_VALUE = 100;
52  
53      private static final int FIRST_TWO_CHARACTER_VALUE = 10;
54  
55      private static final int FITS_DATE_STRING_SIZE = 23;
56  
57      private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
58  
59      private static final int NEW_FORMAT_DAY_OF_MONTH_GROUP = 4;
60  
61      private static final int NEW_FORMAT_HOUR_GROUP = 6;
62  
63      private static final int NEW_FORMAT_MILLISECOND_GROUP = 10;
64  
65      private static final int NEW_FORMAT_MINUTE_GROUP = 7;
66  
67      private static final int NEW_FORMAT_MONTH_GROUP = 3;
68  
69      private static final int NEW_FORMAT_SECOND_GROUP = 8;
70  
71      private static final int NEW_FORMAT_YEAR_GROUP = 2;
72  
73      private static final Pattern NORMAL_REGEX = Pattern.compile(
74              "\\s*(([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9]))(T([0-9][0-9]):([0-9][0-9]):([0-9][0-9])(\\.([0-9]+))?)?\\s*");
75  
76      private static final int OLD_FORMAT_DAY_OF_MONTH_GROUP = 1;
77  
78      private static final int OLD_FORMAT_MONTH_GROUP = 2;
79  
80      private static final int OLD_FORMAT_YEAR_GROUP = 3;
81  
82      private static final Pattern OLD_REGEX = Pattern.compile("\\s*([0-9][0-9])/([0-9][0-9])/([0-9][0-9])\\s*");
83  
84      private static final int YEAR_OFFSET = 1900;
85  
86      private static final int NB_DIGITS_MILLIS = 3;
87  
88      private static final int POW_TEN = 10;
89  
90      /**
91       * Returns the FITS date string for the current date and time.
92       * 
93       * @return the current date in FITS date format
94       * 
95       * @see    #getFitsDateString(Date)
96       */
97      public static String getFitsDateString() {
98          return getFitsDateString(new Date(), true);
99      }
100 
101     /**
102      * Returns the FITS date string for a specific date and time
103      * 
104      * @return       a created FITS format date string Java Date object.
105      *
106      * @param  epoch The epoch to be converted to FITS format.
107      * 
108      * @see          #getFitsDateString(Date, boolean)
109      * @see          #getFitsDateString()
110      */
111     public static String getFitsDateString(Date epoch) {
112         return getFitsDateString(epoch, true);
113     }
114 
115     /**
116      * Returns the FITS date string, with or without the time component, for a specific date and time.
117      * 
118      * @return           a created FITS format date string. Note that the date is not rounded.
119      *
120      * @param  epoch     The epoch to be converted to FITS format.
121      * @param  timeOfDay Whether the time of day information shouldd be included
122      * 
123      * @see              #getFitsDateString(Date)
124      * @see              #getFitsDateString()
125      */
126     public static String getFitsDateString(Date epoch, boolean timeOfDay) {
127         Calendar cal = Calendar.getInstance(UTC);
128         cal.setTime(epoch);
129         StringBuilder fitsDate = new StringBuilder();
130         DecimalFormat df = new DecimalFormat("0000");
131         fitsDate.append(df.format(cal.get(Calendar.YEAR)));
132         fitsDate.append("-");
133         df = new DecimalFormat("00");
134 
135         fitsDate.append(df.format(cal.get(Calendar.MONTH) + 1));
136         fitsDate.append("-");
137         fitsDate.append(df.format(cal.get(Calendar.DAY_OF_MONTH)));
138 
139         if (timeOfDay) {
140             fitsDate.append("T");
141             fitsDate.append(df.format(cal.get(Calendar.HOUR_OF_DAY)));
142             fitsDate.append(":");
143             fitsDate.append(df.format(cal.get(Calendar.MINUTE)));
144             fitsDate.append(":");
145             fitsDate.append(df.format(cal.get(Calendar.SECOND)));
146             fitsDate.append(".");
147             df = new DecimalFormat("000");
148             fitsDate.append(df.format(cal.get(Calendar.MILLISECOND)));
149         }
150         return fitsDate.toString();
151     }
152 
153     private int hour = -1;
154 
155     private int mday = -1;
156 
157     private int millisecond = -1;
158 
159     private int minute = -1;
160 
161     private int month = -1;
162 
163     private int second = -1;
164 
165     private int year = -1;
166 
167     /**
168      * Convert a FITS date string to a Java <CODE>Date</CODE> object.
169      *
170      * @param  dStr          the FITS date
171      *
172      * @throws FitsException if <CODE>dStr</CODE> does not contain a valid FITS date.
173      */
174     public FitsDate(String dStr) throws FitsException {
175         // if the date string is null, we are done
176         if (dStr == null || dStr.isEmpty()) {
177             return;
178         }
179         Matcher match = FitsDate.NORMAL_REGEX.matcher(dStr);
180         if (match.matches()) {
181             year = getInt(match, FitsDate.NEW_FORMAT_YEAR_GROUP);
182             month = getInt(match, FitsDate.NEW_FORMAT_MONTH_GROUP);
183             mday = getInt(match, FitsDate.NEW_FORMAT_DAY_OF_MONTH_GROUP);
184             hour = getInt(match, FitsDate.NEW_FORMAT_HOUR_GROUP);
185             minute = getInt(match, FitsDate.NEW_FORMAT_MINUTE_GROUP);
186             second = getInt(match, FitsDate.NEW_FORMAT_SECOND_GROUP);
187             millisecond = getMilliseconds(match, FitsDate.NEW_FORMAT_MILLISECOND_GROUP);
188         } else {
189             match = FitsDate.OLD_REGEX.matcher(dStr);
190             if (!match.matches()) {
191                 if (dStr.trim().isEmpty()) {
192                     return;
193                 }
194                 throw new FitsException("Bad FITS date string \"" + dStr + '"');
195             }
196             year = getInt(match, FitsDate.OLD_FORMAT_YEAR_GROUP) + FitsDate.YEAR_OFFSET;
197             month = getInt(match, FitsDate.OLD_FORMAT_MONTH_GROUP);
198             mday = getInt(match, FitsDate.OLD_FORMAT_DAY_OF_MONTH_GROUP);
199         }
200     }
201 
202     private static int getInt(Matcher match, int groupIndex) {
203         String value = match.group(groupIndex);
204         if (value != null) {
205             return Integer.parseInt(value);
206         }
207         return -1;
208     }
209 
210     private static int getMilliseconds(Matcher match, int groupIndex) {
211         String value = match.group(groupIndex);
212         if (value != null) {
213             value = String.format("%-3s", value).replace(' ', '0');
214             int num = Integer.parseInt(value);
215             if (value.length() > NB_DIGITS_MILLIS) {
216                 num = (int) Math.round(num / Math.pow(POW_TEN, value.length() - NB_DIGITS_MILLIS));
217             }
218             return num;
219         }
220         return -1;
221     }
222 
223     /**
224      * Get a Java Date object corresponding to this FITS date.
225      *
226      * @return The Java Date object.
227      */
228     public Date toDate() {
229         if (year == -1) {
230             return null;
231         }
232 
233         Calendar cal = Calendar.getInstance(UTC);
234 
235         cal.set(Calendar.YEAR, year);
236         cal.set(Calendar.MONTH, month - 1);
237         cal.set(Calendar.DAY_OF_MONTH, mday);
238 
239         if (hour == -1) {
240             cal.set(Calendar.HOUR_OF_DAY, 0);
241             cal.set(Calendar.MINUTE, 0);
242             cal.set(Calendar.SECOND, 0);
243             cal.set(Calendar.MILLISECOND, 0);
244         } else {
245             cal.set(Calendar.HOUR_OF_DAY, hour);
246             cal.set(Calendar.MINUTE, minute);
247             cal.set(Calendar.SECOND, second);
248             if (millisecond == -1) {
249                 cal.set(Calendar.MILLISECOND, 0);
250             } else {
251                 cal.set(Calendar.MILLISECOND, millisecond);
252             }
253         }
254         return cal.getTime();
255     }
256 
257     @Override
258     public String toString() {
259         if (year == -1) {
260             return "";
261         }
262         StringBuilder buf = new StringBuilder(FitsDate.FITS_DATE_STRING_SIZE);
263         buf.append(year);
264         buf.append('-');
265         appendTwoDigitValue(buf, month);
266         buf.append('-');
267         appendTwoDigitValue(buf, mday);
268         if (hour != -1) {
269             buf.append('T');
270             appendTwoDigitValue(buf, hour);
271             buf.append(':');
272             appendTwoDigitValue(buf, minute);
273             buf.append(':');
274             appendTwoDigitValue(buf, second);
275             if (millisecond != -1) {
276                 buf.append('.');
277                 appendThreeDigitValue(buf, millisecond);
278             }
279         }
280         return buf.toString();
281     }
282 
283     @Override
284     public boolean equals(Object o) {
285         if (o == this) {
286             return true;
287         }
288         if (!(o instanceof FitsDate)) {
289             return false;
290         }
291 
292         return compareTo((FitsDate) o) == 0;
293     }
294 
295     @Override
296     public int hashCode() {
297         return Integer.hashCode(year) ^ Integer.hashCode(month) ^ Integer.hashCode(mday) ^ Integer.hashCode(hour)
298                 ^ Integer.hashCode(minute) ^ Integer.hashCode(second) ^ Integer.hashCode(millisecond);
299     }
300 
301     @Override
302     public int compareTo(FitsDate fitsDate) {
303         int result = Integer.compare(year, fitsDate.year);
304         if (result != 0) {
305             return result;
306         }
307 
308         result = Integer.compare(month, fitsDate.month);
309         if (result != 0) {
310             return result;
311         }
312 
313         result = Integer.compare(mday, fitsDate.mday);
314         if (result != 0) {
315             return result;
316         }
317 
318         result = Integer.compare(hour, fitsDate.hour);
319         if (result != 0) {
320             return result;
321         }
322 
323         result = Integer.compare(minute, fitsDate.minute);
324         if (result != 0) {
325             return result;
326         }
327 
328         result = Integer.compare(second, fitsDate.second);
329         if (result != 0) {
330             return result;
331         }
332 
333         return Integer.compare(millisecond, fitsDate.millisecond);
334     }
335 
336     private void appendThreeDigitValue(StringBuilder buf, int value) {
337         if (value < FitsDate.FIRST_THREE_CHARACTER_VALUE) {
338             buf.append('0');
339         }
340         appendTwoDigitValue(buf, value);
341     }
342 
343     private void appendTwoDigitValue(StringBuilder buf, int value) {
344         if (value < FitsDate.FIRST_TWO_CHARACTER_VALUE) {
345             buf.append('0');
346         }
347         buf.append(value);
348     }
349 }