1   /* Copyright 2002-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS 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.orekit.time;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.InputStreamReader;
23  import java.nio.charset.StandardCharsets;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import org.hipparchus.util.FastMath;
30  import org.orekit.annotation.DefaultDataContext;
31  import org.orekit.data.AbstractSelfFeedingLoader;
32  import org.orekit.data.DataContext;
33  import org.orekit.data.DataProvidersManager;
34  import org.orekit.errors.OrekitException;
35  import org.orekit.errors.OrekitMessages;
36  
37  /** Loader for UTC-TAI extracted from tai-utc.dat file from USNO.
38   * <p>
39   * This class is immutable and hence thread-safe
40   * </p>
41   * @author Luc Maisonobe
42   * @since 7.1
43   */
44  public class TAIUTCDatFilesLoader extends AbstractSelfFeedingLoader
45          implements UTCTAIOffsetsLoader {
46  
47      /** Default supported files name pattern. */
48      public static final String DEFAULT_SUPPORTED_NAMES = "^tai-utc\\.dat$";
49  
50      /**
51       * Build a loader for tai-utc.dat file from USNO. This constructor uses the {@link
52       * DataContext#getDefault() default data context}.
53       *
54       * @param supportedNames regular expression for supported files names
55       * @see #TAIUTCDatFilesLoader(String, DataProvidersManager)
56       */
57      @DefaultDataContext
58      public TAIUTCDatFilesLoader(final String supportedNames) {
59          this(supportedNames, DataContext.getDefault().getDataProvidersManager());
60      }
61  
62      /**
63       * Build a loader for tai-utc.dat file from USNO.
64       *
65       * @param supportedNames regular expression for supported files names
66       * @param manager        provides access to the {@code tai-utc.dat} file.
67       */
68      public TAIUTCDatFilesLoader(final String supportedNames,
69                                  final DataProvidersManager manager) {
70          super(supportedNames, manager);
71      }
72  
73      /** {@inheritDoc} */
74      @Override
75      public List<OffsetModel> loadOffsets() {
76          final UtcTaiOffsetLoader parser = new UtcTaiOffsetLoader(new Parser());
77          this.feed(parser);
78          return parser.getOffsets();
79      }
80  
81      /** Internal class performing the parsing. */
82      public static class Parser implements UTCTAIOffsetsLoader.Parser {
83  
84          /** Number of seconds in one day. */
85          private static final long SEC_PER_DAY = 86400L;
86  
87          /** Number of attoseconds in one second. */
88          private static final long ATTOS_PER_NANO = 1000000000L;
89  
90          /** Slope conversion factor from seconds per day to nanoseconds per second. */
91          private static final long SLOPE_FACTOR = SEC_PER_DAY * ATTOS_PER_NANO;
92  
93          /** Regular expression for optional blanks. */
94          private static final String BLANKS               = "\\p{Blank}*";
95  
96          /** Regular expression for storage start. */
97          private static final String STORAGE_START        = "(";
98  
99          /** Regular expression for storage end. */
100         private static final String STORAGE_END          = ")";
101 
102         /** Regular expression for alternative. */
103         private static final String ALTERNATIVE          = "|";
104 
105         /** Regular expression matching blanks at start of line. */
106         private static final String LINE_START_REGEXP     = "^" + BLANKS;
107 
108         /** Regular expression matching blanks at end of line. */
109         private static final String LINE_END_REGEXP       = BLANKS + "$";
110 
111         /** Regular expression matching integers. */
112         private static final String INTEGER_REGEXP        = "[-+]?\\p{Digit}+";
113 
114         /** Regular expression matching real numbers. */
115         private static final String REAL_REGEXP           = "[-+]?(?:\\p{Digit}+(?:\\.\\p{Digit}*)?|\\.\\p{Digit}+)(?:[eE][-+]?\\p{Digit}+)?";
116 
117         /** Regular expression matching an integer field to store. */
118         private static final String STORED_INTEGER_FIELD  = BLANKS + STORAGE_START + INTEGER_REGEXP + STORAGE_END;
119 
120         /** Regular expression matching a real field to store. */
121         private static final String STORED_REAL_FIELD     = BLANKS + STORAGE_START + REAL_REGEXP + STORAGE_END;
122 
123         /** Data lines pattern. */
124         private final Pattern dataPattern;
125 
126         /** Simple constructor.
127          */
128         public Parser() {
129 
130             // data lines read:
131             // 1965 SEP  1 =JD 2439004.5  TAI-UTC=   3.8401300 S + (MJD - 38761.) X 0.001296 S
132             // 1966 JAN  1 =JD 2439126.5  TAI-UTC=   4.3131700 S + (MJD - 39126.) X 0.002592 S
133             // 1968 FEB  1 =JD 2439887.5  TAI-UTC=   4.2131700 S + (MJD - 39126.) X 0.002592 S
134             // 1972 JAN  1 =JD 2441317.5  TAI-UTC=  10.0       S + (MJD - 41317.) X 0.0      S
135             // 1972 JUL  1 =JD 2441499.5  TAI-UTC=  11.0       S + (MJD - 41317.) X 0.0      S
136             // 1973 JAN  1 =JD 2441683.5  TAI-UTC=  12.0       S + (MJD - 41317.) X 0.0      S
137             // 1974 JAN  1 =JD 2442048.5  TAI-UTC=  13.0       S + (MJD - 41317.) X 0.0      S
138 
139             // month as a three letters upper case abbreviation
140             final StringBuilder builder = new StringBuilder(BLANKS + STORAGE_START);
141             for (final Month month : Month.values()) {
142                 builder.append(month.getUpperCaseAbbreviation());
143                 builder.append(ALTERNATIVE);
144             }
145             builder.delete(builder.length() - 1, builder.length());
146             builder.append(STORAGE_END);
147             final String monthField = builder.toString();
148 
149             dataPattern = Pattern.compile(LINE_START_REGEXP +
150                                           STORED_INTEGER_FIELD + monthField + STORED_INTEGER_FIELD +
151                                           "\\p{Blank}+=JD" + STORED_REAL_FIELD +
152                                           "\\p{Blank}+TAI-UTC=" + STORED_REAL_FIELD +
153                                           "\\p{Blank}+S\\p{Blank}+\\+\\p{Blank}+\\(MJD\\p{Blank}+-" + STORED_REAL_FIELD +
154                                           "\\p{Blank}*\\)\\p{Blank}+X" + STORED_REAL_FIELD +
155                                           "\\p{Blank}*S" + LINE_END_REGEXP);
156 
157 
158         }
159 
160         /** Load UTC-TAI offsets entries read from some file.
161          * <p>The time steps are extracted from some {@code tai-utc.dat} file.
162          * Since entries are stored in a {@link java.util.SortedMap SortedMap},
163          * they are chronologically sorted and only one entry remains for a given date.</p>
164          * @param input data input stream
165          * @param name name of the file (or zip entry)
166          * @exception IOException if data can't be read
167          */
168         @Override
169         public List<OffsetModel> parse(final InputStream input, final String name)
170             throws IOException {
171 
172             final List<OffsetModel> offsets = new ArrayList<>();
173 
174             int lineNumber = 0;
175             DateComponents lastDate = null;
176             String line = null;
177             // set up a reader for line-oriented file
178             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
179 
180                 // read all file, ignoring not recognized lines
181                 for (line = reader.readLine(); line != null; line = reader.readLine()) {
182                     ++lineNumber;
183 
184                     // check matching for data lines
185                     final Matcher matcher = dataPattern.matcher(line);
186                     if (matcher.matches()) {
187 
188                         // build an entry from the extracted fields
189                         final DateComponents dc1 = new DateComponents(Integer.parseInt(matcher.group(1)),
190                                                                       Month.parseMonth(matcher.group(2)),
191                                                                       Integer.parseInt(matcher.group(3)));
192                         final DateComponents dc2 = new DateComponents(DateComponents.JULIAN_EPOCH,
193                                                                       (int) FastMath.ceil(Double.parseDouble(matcher.group(4))));
194                         if (!dc1.equals(dc2)) {
195                             throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
196                                                       name, dc1.getYear(), dc1.getMonth(), dc1.getDay(), dc2.getMJD());
197                         }
198 
199                         if (lastDate != null && dc1.compareTo(lastDate) <= 0) {
200                             throw new OrekitException(OrekitMessages.NON_CHRONOLOGICAL_DATES_IN_FILE,
201                                                       name, lineNumber);
202                         }
203                         lastDate = dc1;
204 
205                         final double mjdRef = Double.parseDouble(matcher.group(6));
206                         offsets.add(new OffsetModel(dc1, (int) FastMath.rint(mjdRef),
207                                                     TimeOffset.parse(matcher.group(5)),
208                                                     (int) (TimeOffset.parse(matcher.group(7)).getAttoSeconds() / SLOPE_FACTOR)));
209 
210                     }
211                 }
212 
213             } catch (NumberFormatException nfe) {
214                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
215                                           lineNumber, name, line);
216             }
217 
218             if (offsets.isEmpty()) {
219                 throw new OrekitException(OrekitMessages.NO_ENTRIES_IN_IERS_UTC_TAI_HISTORY_FILE, name);
220             }
221 
222             return offsets;
223 
224         }
225 
226     }
227 
228 }