1   /* Copyright 2002-2026 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.frames;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.function.Supplier;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import org.hipparchus.util.FastMath;
31  import org.hipparchus.util.RomanNumeral;
32  import org.orekit.data.DataProvidersManager;
33  import org.orekit.data.DataSource;
34  import org.orekit.errors.OrekitException;
35  import org.orekit.errors.OrekitInternalError;
36  import org.orekit.errors.OrekitMessages;
37  import org.orekit.time.AbsoluteDate;
38  import org.orekit.time.DateComponents;
39  import org.orekit.time.Month;
40  import org.orekit.time.TimeScale;
41  import org.orekit.utils.IERSConventions;
42  import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
43  import org.orekit.utils.units.UnitsConverter;
44  
45  /** Loader for bulletin A files.
46   * <p>Bulletin A files contain {@link EOPEntry
47   * Earth Orientation Parameters} for a few days periods, they
48   * correspond to rapid data estimations, suitable for near-real time
49   * and prediction purposes. Prediction series are only available for
50   * pole motion xp, yp and UT1-UTC, they are not available for
51   * pole offsets (Δδψ/Δδε and x/y).</p>
52   * <p>A bulletin A published on Modified Julian Day mjd (nominally a
53   * Thursday) will generally contain:
54   * </p>
55   * <ul>
56   *   <li>rapid service xp, yp and UT1-UTC data from mjd-6 to mjd</li>
57   *   <li>prediction xp, yp and UT1-UTC data from mjd+1 to mjd+365</li>
58   *   <li>if it is first bulletin of month m, final values xp, yp and
59   *       UT1-UTC data from day 2 of month m-2 to day 1 of month m-1</li>
60   *   <li>rapid service pole offsets Δδψ/Δδε and x/y if available, for some
61   *       varying period somewhere from mjd-30 to mjd-10 (see below)</li>
62   *   <li>if it is first bulletin of month m, final values pole offsets
63   *       Δδψ/Δδε and x/y data from day 2 of month m-2 to day 1 of month
64   *       m-1</li>
65   * </ul>
66   * <p>
67   * There are some discrepancies in the rapid service time range above,
68   * mainly when the nominal publication Thursday corresponds to holidays.
69   * In this case a bulletin may be published the day before and have a 6
70   * days span only for rapid data, and a later bulletin will have an 8 days
71   * span to recover the normal schedule. This occurred for bulletin A Vol.
72   * XVIII No. 047, bulletin A Vol. XVIII No. 048, bulletin A Vol. XXI No.
73   * 052 and bulletin A Vol. XXII No. 001.
74   * </p>
75   * <p>Rapid service for pole offsets appears irregular. As extreme examples
76   * bulletin A Vol. XXVI No. 037 from 2013-09-12 contained 15 entries
77   * for pole offsets, from mjd-22 to mjd-8, bulletin A Vol. XXVI No. 039
78   * from 2013-09-26 contained only 3 entries for pole offsets, from mjd-15
79   * to mjd-13, and bulletin A Vol. XXVI No. 040 from 2013-10-03 contained no
80   * rapid service pole offsets at all, it contained only final values. Despite
81   * this irregularity, rapid service data is continuous over consecutive files,
82   * so the average number of entries is 7 as the files are published on a weekly
83   * basis.
84   * </p>
85   * <p>
86   * There are no prediction data for pole offsets.
87   * </p>
88   * <p>
89   * This loader reads both the rapid service, the prediction and the final
90   * values parts. As successive files have overlaps between all these sections,
91   * values extracted from latest files (according to the publication dates from header)
92   * override values extracted from earlier files, regardless of the files
93   * reading order. If numerous bulletins A covering more than one year are read,
94   * one particular date will typically appear:
95   * </p>
96   * <ul>
97   *     <li>in the prediction section of 52 or 53 files,</li>
98   *     <li>then in the rapid data section of one file</li>
99   *     <li>then it will be missing in a few files,</li>
100  *     <li>and finally it will appear in the final values sections of a last file.</li>
101  * </ul>
102  * <p>
103  * In this case, the value retained will be the one extracted from the
104  * final values section in the last published file.
105  * </p>
106  * <p>
107  * If only one bulletin A file is read and if it corresponds to the first bulletin
108  * of a month, it will have a roughly one month wide hole between the
109  * final data and the rapid data. This hole will trigger an error as EOP
110  * continuity is checked by default for at most 5 days holes. In this case,
111  * users should call something like {@link FramesFactory#setEOPContinuityThreshold(double)
112  * FramesFactory.setEOPContinuityThreshold(Constants.JULIAN_YEAR)} to prevent
113  * the error to be triggered.
114  * </p>
115  * <p>
116  * The bulletin A files are recognized thanks to their base names, which match the pattern
117  * {@code bulletina-xxx-###.txt}, (or the same ending with {@code .gz} for gzip-compressed
118  * files). In this pattern, xxx is a roman numeral corresponding to the year minus 1987,
119  * and ### is a decimal number corresponding to the number of the week.
120  * </p>
121  * <p>
122  * The week number is generally the ISO week number (i.e. week 1 is the week that contains the
123  * first Thursday of the year). This was not the case in 2009, as the bulletin A for week 1
124  * was published on Thursday, January 8th (probably because Thursday, January 1st was holidays).
125  * So for whole 2009 year, the week number was ISO week minus one. This discrepancy was not
126  * reproduced for years 2015 and 2026, for both years the bulletin A for week 1 was published
127  * on Thursday, January 1st.
128  * </p>
129  * <p>
130  * Bulletin A in csv format must be read using {@link EopCsvFilesLoader} rather
131  * than using this loader. Bulletin A in xml format must be read using {@link EopXmlLoader}
132  * rather than using this loader.
133  * </p>
134  * <p>
135  * This class is immutable and hence thread-safe
136  * </p>
137  * @author Luc Maisonobe
138  * @since 7.0
139  * @see EopCsvFilesLoader
140  * @see EopXmlLoader
141  */
142 class BulletinAFilesLoader extends AbstractEopLoader implements EopHistoryLoader {
143 
144     /** Regular expression matching blanks at start of line. */
145     private static final String LINE_START_REGEXP     = "^\\p{Blank}+";
146 
147     /** Regular expression matching blanks at end of line. */
148     private static final String LINE_END_REGEXP       = "\\p{Blank}*$";
149 
150     /** Regular expression matching integers. */
151     private static final String INTEGER_REGEXP        = "[-+]?\\p{Digit}+";
152 
153     /** Regular expression matching real numbers. */
154     private static final String REAL_REGEXP           = "[-+]?(?:(?:\\p{Digit}+(?:\\.\\p{Digit}*)?)|(?:\\.\\p{Digit}+))(?:[eE][-+]?\\p{Digit}+)?";
155 
156     /** Regular expression matching an integer field to store. */
157     private static final String STORED_INTEGER_FIELD  = "\\p{Blank}*(" + INTEGER_REGEXP + ")";
158 
159     /** regular expression matching a Modified Julian Day field to store. */
160     private static final String STORED_MJD_FIELD      = "\\p{Blank}+(\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit})";
161 
162     /** Regular expression matching a real field to store. */
163     private static final String STORED_REAL_FIELD     = "\\p{Blank}+(" + REAL_REGEXP + ")";
164 
165     /** Regular expression matching a real field to ignore. */
166     private static final String IGNORED_REAL_FIELD    = "\\p{Blank}+" + REAL_REGEXP;
167 
168     /** Regular expression matching a string field to store. */
169     private static final String STORED_STRING_FIELD   = "\\p{Blank}+(\\p{Alpha}+)";
170 
171     /** Enum for files sections, in expected order.
172      * <p>The bulletin A weekly data files contain several sections,
173      * each introduced with some fixed header text and followed by tabular data.
174      * </p>
175      */
176     private enum Section {
177 
178         /** Publication metadata. */
179         //      **********************************************************************
180         //      *                                                                    *
181         //      *                   I E R S   B U L L E T I N - A                    *
182         //      *                                                                    *
183         //      *           Rapid Service/Prediction of Earth Orientation            *
184         //      **********************************************************************
185         //      5 January 2006                                        Vol. XIX No. 001
186         //      ______________________________________________________________________
187         PUBLICATION_METADATA("\\* *I E R S   B U L L E T I N - A *\\*",
188                              LINE_START_REGEXP +
189                              STORED_INTEGER_FIELD + STORED_STRING_FIELD + STORED_INTEGER_FIELD +
190                              "\\p{Blank}*Vol\\." + STORED_STRING_FIELD +
191                              "\\p{Blank}*No\\." + STORED_INTEGER_FIELD +
192                              LINE_END_REGEXP),
193 
194         /** Earth Orientation Parameters rapid service. */
195         // section 2 always contain rapid service data including error fields
196         //      COMBINED EARTH ORIENTATION PARAMETERS:
197         //
198         //                              IERS Rapid Service
199         //              MJD      x    error     y    error   UT1-UTC   error
200         //                       "      "       "      "        s        s
201         //   13  8 30  56534 0.16762 .00009 0.32705 .00009  0.038697 0.000019
202         //   13  8 31  56535 0.16669 .00010 0.32564 .00010  0.038471 0.000019
203         //   13  9  1  56536 0.16592 .00009 0.32410 .00010  0.038206 0.000024
204         //   13  9  2  56537 0.16557 .00009 0.32270 .00009  0.037834 0.000024
205         //   13  9  3  56538 0.16532 .00009 0.32147 .00010  0.037351 0.000024
206         //   13  9  4  56539 0.16488 .00009 0.32044 .00010  0.036756 0.000023
207         //   13  9  5  56540 0.16435 .00009 0.31948 .00009  0.036036 0.000024
208         EOP_RAPID_SERVICE("COMBINED EARTH ORIENTATION PARAMETERS:",
209                           LINE_START_REGEXP +
210                           STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
211                           STORED_MJD_FIELD +
212                           STORED_REAL_FIELD + IGNORED_REAL_FIELD +
213                           STORED_REAL_FIELD + IGNORED_REAL_FIELD +
214                           STORED_REAL_FIELD + IGNORED_REAL_FIELD +
215                           LINE_END_REGEXP),
216 
217         /** Earth Orientation Parameters final values. */
218         // the first bulletin A of each month also includes final values for the
219         // period covering from day 2 of month m-2 to day 1 of month m-1.
220         //                                IERS Final Values
221         //                                 MJD        x        y      UT1-UTC
222         //                                            "        "         s
223         //             13  7  2           56475    0.1441   0.3901   0.05717
224         //             13  7  3           56476    0.1457   0.3895   0.05716
225         //             13  7  4           56477    0.1467   0.3887   0.05728
226         //             13  7  5           56478    0.1477   0.3875   0.05755
227         //             13  7  6           56479    0.1490   0.3862   0.05793
228         //             13  7  7           56480    0.1504   0.3849   0.05832
229         //             13  7  8           56481    0.1516   0.3835   0.05858
230         //             13  7  9           56482    0.1530   0.3822   0.05877
231         EOP_FINAL_VALUES("IERS Final Values",
232                          LINE_START_REGEXP +
233                          STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
234                          STORED_MJD_FIELD +
235                          STORED_REAL_FIELD +
236                          STORED_REAL_FIELD +
237                          STORED_REAL_FIELD +
238                          LINE_END_REGEXP),
239 
240         /** Earth Orientation Parameters prediction. */
241         // section 3 always contain prediction data without error fields
242         //
243         //         PREDICTIONS:
244         //         The following formulas will not reproduce the predictions given below,
245         //         but may be used to extend the predictions beyond the end of this table.
246         //
247         //         x =  0.0969 + 0.1110 cos A - 0.0103 sin A - 0.0435 cos C - 0.0171 sin C
248         //         y =  0.3457 - 0.0061 cos A - 0.1001 sin A - 0.0171 cos C + 0.0435 sin C
249         //            UT1-UTC = -0.0052 - 0.00104 (MJD - 56548) - (UT2-UT1)
250         //
251         //         where A = 2*pi*(MJD-56540)/365.25 and C = 2*pi*(MJD-56540)/435.
252         //
253         //            TAI-UTC(MJD 56541) = 35.0
254         //         The accuracy may be estimated from the expressions:
255         //         S x,y = 0.00068 (MJD-56540)**0.80   S t = 0.00025 (MJD-56540)**0.75
256         //         Estimated accuracies are:  Predictions     10 d   20 d   30 d   40 d
257         //                                    Polar coord's  0.004  0.007  0.010  0.013
258         //                                    UT1-UTC        0.0014 0.0024 0.0032 0.0040
259         //
260         //                       MJD      x(arcsec)   y(arcsec)   UT1-UTC(sec)
261         //          2013  9  6  56541       0.1638      0.3185      0.03517
262         //          2013  9  7  56542       0.1633      0.3175      0.03420
263         //          2013  9  8  56543       0.1628      0.3164      0.03322
264         //          2013  9  9  56544       0.1623      0.3153      0.03229
265         //          2013  9 10  56545       0.1618      0.3142      0.03144
266         //          2013  9 11  56546       0.1612      0.3131      0.03071
267         //          2013  9 12  56547       0.1607      0.3119      0.03008
268         EOP_PREDICTION("PREDICTIONS:",
269                        LINE_START_REGEXP +
270                        STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
271                        STORED_MJD_FIELD +
272                        STORED_REAL_FIELD +
273                        STORED_REAL_FIELD +
274                        STORED_REAL_FIELD +
275                        LINE_END_REGEXP),
276 
277         /** Pole offsets, IAU-1980. */
278         // section 4 may contain rapid service pole offset series including error fields
279         //        CELESTIAL POLE OFFSET SERIES:
280         //                             NEOS Celestial Pole Offset Series
281         //                         MJD      dpsi    error     deps    error
282         //                                          (msec. of arc)
283         //                        56519   -87.47     0.13   -12.96     0.08
284         //                        56520   -87.72     0.13   -13.20     0.08
285         //                        56521   -87.79     0.19   -13.56     0.11
286         POLE_OFFSETS_IAU_1980_RAPID_SERVICE("NEOS Celestial Pole Offset Series",
287                                             LINE_START_REGEXP +
288                                             STORED_MJD_FIELD +
289                                             STORED_REAL_FIELD + IGNORED_REAL_FIELD +
290                                             STORED_REAL_FIELD + IGNORED_REAL_FIELD +
291                                             LINE_END_REGEXP),
292 
293         /** Pole offsets, IAU-1980 final values. */
294         // the first bulletin A of each month also includes final values for the
295         // period covering from day 2 of month m-2 to day 1 of month m-1.
296         //                    IERS Celestial Pole Offset Final Series
297         //                          MJD          dpsi      deps
298         //                                       (msec. of arc)
299         //                         56475       -81.0     -13.3
300         //                         56476       -81.2     -13.4
301         //                         56477       -81.6     -13.4
302         //                         56478       -82.2     -13.5
303         //                         56479       -82.5     -13.6
304         //                         56480       -82.5     -13.7
305         POLE_OFFSETS_IAU_1980_FINAL_VALUES("IERS Celestial Pole Offset Final Series",
306                                            LINE_START_REGEXP +
307                                            STORED_MJD_FIELD +
308                                            STORED_REAL_FIELD +
309                                            STORED_REAL_FIELD +
310                                            LINE_END_REGEXP),
311 
312         /** Pole offsets, IAU-2000. */
313         // the format for the IAU-2000 series is similar, but the meanings of the fields
314         // are different
315         //                       IAU2000A Celestial Pole Offset Series
316         //                        MJD      dX     error     dY     error
317         //                                      (msec. of arc)
318         //                       56519   -0.246   0.052   -0.223   0.080
319         //                       56520   -0.239   0.052   -0.248   0.080
320         //                       56521   -0.224   0.076   -0.277   0.110
321         POLE_OFFSETS_IAU_2000_RAPID_SERVICE("IAU2000A Celestial Pole Offset Series",
322                                             LINE_START_REGEXP +
323                                             STORED_MJD_FIELD +
324                                             STORED_REAL_FIELD + IGNORED_REAL_FIELD +
325                                             STORED_REAL_FIELD + IGNORED_REAL_FIELD +
326                                             LINE_END_REGEXP),
327 
328         /** Pole offsets, IAU-2000 final values. */
329         // the format for the IAU-2000 series is similar, but the meanings of the fields
330         // are different
331         //                   IAU2000A Celestial Pole Offset Final Series
332         //                            MJD     dX         dY
333         //                            (msec. of arc)
334         //                          56475     0.00      -0.28
335         //                          56476    -0.06      -0.29
336         //                          56477    -0.07      -0.27
337         //                          56478    -0.12      -0.33
338         //                          56479    -0.12      -0.33
339         //                          56480    -0.13      -0.36
340         POLE_OFFSETS_IAU_2000_FINAL_VALUES("IAU2000A Celestial Pole Offset Final Series",
341                                            LINE_START_REGEXP +
342                                            STORED_MJD_FIELD +
343                                            STORED_REAL_FIELD +
344                                            STORED_REAL_FIELD +
345                                            LINE_END_REGEXP);
346 
347         /** Header pattern. */
348         private final Pattern header;
349 
350         /** Data pattern. */
351         private final Pattern data;
352 
353         /** Simple constructor.
354          * @param headerRegExp regular expression for header
355          * @param dataRegExp regular expression for data
356          */
357         Section(final String headerRegExp, final String dataRegExp) {
358             this.header = Pattern.compile(LINE_START_REGEXP + headerRegExp + LINE_END_REGEXP);
359             this.data   = Pattern.compile(dataRegExp);
360         }
361 
362         /** Check if a line matches the section header.
363          * @param line line to check
364          * @return true if the line matches the header
365          */
366         public boolean matchesHeader(final String line) {
367             return header.matcher(line).matches();
368         }
369 
370         /** Get the data fields from a line.
371          * @param line line to parse
372          * @return extracted fields, or null if line does not match data format
373          */
374         public String[] getFields(final String line) {
375             final Matcher matcher = data.matcher(line);
376             if (matcher.matches()) {
377                 final String[] fields = new String[matcher.groupCount()];
378                 for (int i = 0; i < fields.length; ++i) {
379                     fields[i] = matcher.group(i + 1);
380                 }
381                 return fields;
382             } else {
383                 return null;
384             }
385         }
386 
387     }
388 
389     /** Build a loader for IERS bulletins A files.
390      * @param supportedNames regular expression for supported files names
391      * @param manager provides access to the bulletin A files.
392      * @param utcSupplier UTC time scale.
393      */
394     BulletinAFilesLoader(final String supportedNames,
395                          final DataProvidersManager manager,
396                          final Supplier<TimeScale> utcSupplier) {
397         super(supportedNames, manager, utcSupplier);
398     }
399 
400     /** {@inheritDoc} */
401     public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
402                             final Collection<EOPEntry> history) {
403         final ItrfVersionProvider itrfVersionProvider =
404             new ITRFVersionLoader(ITRFVersionLoader.SUPPORTED_NAMES, getDataProvidersManager());
405         final Parser parser = new Parser(converter, itrfVersionProvider, getUtc());
406         final EopParserLoader loader = new EopParserLoader(parser);
407         this.feed(loader);
408 
409         history.addAll(loader.getEop());
410 
411     }
412 
413     /** Internal class performing the parsing. */
414     static class Parser extends AbstractEopParser {
415 
416         /** Constant for dummy origin.
417          * @since 14.0
418          */
419         private static final EOPOrigin DUMMY_ORIGIN =
420             new EOPOrigin(DateComponents.MODIFIED_JULIAN_EPOCH, "dummy");
421 
422         /** File name. */
423         private String fileName;
424 
425         /** Current line number. */
426         private int lineNumber;
427 
428         /** Current line. */
429         private String line;
430 
431         /** Origin.
432          * @since 14.0
433          */
434         private EOPOrigin origin;
435 
436         /** Earliest parsed data. */
437         private int mjdMin;
438 
439         /** Latest parsed data. */
440         private int mjdMax;
441 
442         /** Simple constructor.
443          * @param converter           converter to use
444          * @param itrfVersionProvider to use for determining the ITRF version of the EOP.
445          * @param utc                 time scale for parsing dates.
446          * @since 14.0
447          */
448         Parser(final NutationCorrectionConverter converter,
449                final ItrfVersionProvider itrfVersionProvider,
450                final TimeScale utc) {
451             super(converter, itrfVersionProvider, utc);
452             this.lineNumber = 0;
453             this.origin     = DUMMY_ORIGIN;
454             this.mjdMin     = Integer.MAX_VALUE;
455             this.mjdMax     = Integer.MIN_VALUE;
456         }
457 
458         /** {@inheritDoc} */
459         @Override
460         public List<EOPEntry> parse(final DataSource source)
461             throws IOException {
462 
463             // create a new list for entries parsed from this file
464             final List<EOPEntry> eop = new ArrayList<>();
465 
466             this.fileName      = source.getName();
467 
468             // set up a reader for line-oriented bulletin A files
469             try (BufferedReader reader = new BufferedReader(source.getOpener().openReaderOnce())) {
470                 lineNumber =  0;
471 
472                 // loop over sections
473                 final List<Section> remaining = new ArrayList<>(Arrays.asList(Section.values()));
474                 for (Section section = nextSection(remaining, reader);
475                      section != null;
476                      section = nextSection(remaining, reader)) {
477 
478                     final EopDataType eopDataType = switch (section) {
479                         case PUBLICATION_METADATA -> null;
480                         case EOP_FINAL_VALUES, POLE_OFFSETS_IAU_1980_FINAL_VALUES, POLE_OFFSETS_IAU_2000_FINAL_VALUES -> EopDataType.FINAL;
481                         case EOP_RAPID_SERVICE, POLE_OFFSETS_IAU_1980_RAPID_SERVICE,
482                              POLE_OFFSETS_IAU_2000_RAPID_SERVICE -> EopDataType.RAPID;
483                         case EOP_PREDICTION -> EopDataType.PREDICTED;
484                     };
485 
486                     switch (section) {
487                         case PUBLICATION_METADATA:
488                             loadMetadata(section, reader);
489                             break;
490                         case EOP_RAPID_SERVICE :
491                         case EOP_FINAL_VALUES  :
492                         case EOP_PREDICTION    :
493                             eop.addAll(loadXYDT(section, reader, eopDataType));
494                             break;
495                         case POLE_OFFSETS_IAU_1980_RAPID_SERVICE :
496                         case POLE_OFFSETS_IAU_1980_FINAL_VALUES  :
497                             eop.addAll(loadPoleOffsets(section, false, reader, eopDataType));
498                             break;
499                         case POLE_OFFSETS_IAU_2000_RAPID_SERVICE :
500                         case POLE_OFFSETS_IAU_2000_FINAL_VALUES  :
501                             eop.addAll(loadPoleOffsets(section, true, reader, eopDataType));
502                             break;
503                         default :
504                             // this should never happen
505                             throw new OrekitInternalError(null);
506                     }
507 
508                     // remove the already parsed section from the list
509                     remaining.remove(section);
510 
511                 }
512 
513                 // check that the mandatory sections have been parsed
514                 if (remaining.contains(Section.EOP_RAPID_SERVICE) ||
515                     remaining.contains(Section.EOP_PREDICTION) ||
516                     (remaining.contains(Section.POLE_OFFSETS_IAU_1980_RAPID_SERVICE) ^
517                      remaining.contains(Section.POLE_OFFSETS_IAU_2000_RAPID_SERVICE)) ||
518                     (remaining.contains(Section.POLE_OFFSETS_IAU_1980_FINAL_VALUES) ^
519                      remaining.contains(Section.POLE_OFFSETS_IAU_2000_FINAL_VALUES))) {
520                     throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, source.getName());
521                 }
522 
523             }
524 
525             // return the parsed eop
526             return eop;
527 
528         }
529 
530         /** Skip to next section header.
531          * @param sections sections to check for
532          * @param reader reader from where file content is obtained
533          * @return the next section or null if no section is found until end of file
534          * @exception IOException if data can't be read
535          */
536         private Section nextSection(final List<Section> sections,
537                                     final BufferedReader reader)
538             throws IOException {
539 
540             for (line = reader.readLine(); line != null; line = reader.readLine()) {
541                 ++lineNumber;
542                 for (Section section : sections) {
543                     if (section.matchesHeader(line)) {
544                         return section;
545                     }
546                 }
547             }
548 
549             // we have reached end of file and not found a matching section header
550             return null;
551 
552         }
553 
554         /** Read publication metadata.
555          * @param section section to parse
556          * @param reader reader from where file content is obtained
557          * @exception IOException if data can't be read
558          */
559         private void loadMetadata(final Section section, final BufferedReader reader)
560             throws IOException {
561             for (line = reader.readLine(); line != null; line = reader.readLine()) {
562                 lineNumber++;
563                 final String[] fields = section.getFields(line);
564                 if (fields != null) {
565 
566                     // parse the date
567                     final DateComponents publicationDate = new DateComponents(Integer.parseInt(fields[2]),
568                                                                               Month.parseMonth(fields[1]),
569                                                                               Integer.parseInt(fields[0]));
570                     origin = new EOPOrigin(publicationDate, fileName);
571 
572                     // check volume and week number
573                     checkVolume(RomanNumeral.parse(fields[3].toUpperCase(Locale.ROOT)));
574                     checkWeek(Integer.parseInt(fields[4]));
575 
576                     return;
577 
578                 }
579             }
580         }
581 
582         /** Check the volume number against publication date.
583          * @param parsed parsed volume
584          */
585         private void checkVolume(final int parsed) {
586             final int computed = origin.publicationDate().getYear() - 1987;
587             if (computed != parsed) {
588                 throw new OrekitException(OrekitMessages.UNEXPECTED_DATA_AT_LINE_IN_FILE,
589                                           lineNumber, fileName);
590             }
591         }
592 
593         /** Check the week number against publication date.
594          * @param parsed parsed week
595          */
596         private void checkWeek(final int parsed) {
597 
598             final int computed = origin.publicationDate().getCalendarWeek();
599 
600             if (computed != parsed) {
601 
602                 // in 2009, January 1st was a Thursday, and the bulletin A numbered as week 1
603                 // was published on January 8th. Week numbers in bulletin A were therefore ISO
604                 // week number minus 1 throughout the 2009 year. This did not happen in 2015
605                 // or 2026 despite both years also started on a Thursday
606                 // The previous file published on 2008-12-31 was also numbered as week 53 in
607                 // 2008, despite its ISO number was really 1 in 2009.
608                 // we allow these special cases here
609                 final boolean januaryFirstIsThursday =
610                     new DateComponents(origin.publicationDate().getYear(), 1, 1).getDayOfWeek() == 4;
611                 if (januaryFirstIsThursday && parsed == computed - 1 ||
612                     computed == 1 && parsed == 53) {
613                     // we accept this sloppy week number
614                     return;
615                 }
616 
617                 throw new OrekitException(OrekitMessages.UNEXPECTED_DATA_AT_LINE_IN_FILE,
618                                           lineNumber, fileName);
619 
620             }
621 
622         }
623 
624         /** Read X, Y, UT1-UTC.
625          * @param section section to parse
626          * @param reader reader from where file content is obtained
627          * @param eopDataType EOP data type
628          * @return parsed incomplete EOP
629          * @exception IOException if data can't be read
630          */
631         private List<EOPEntry> loadXYDT(final Section section, final BufferedReader reader,
632                                         final EopDataType eopDataType)
633             throws IOException {
634 
635             final List<EOPEntry> eop = new ArrayList<>();
636             for (line = reader.readLine(); line != null; line = reader.readLine()) {
637 
638                 lineNumber++;
639                 final String[] fields = section.getFields(line);
640                 if (fields == null) {
641                     if (eop.isEmpty()) {
642                         // we are still waiting for the first data line
643                         continue;
644                     } else {
645                         // we have reach a separation line with next section
646                         break;
647                     }
648                 }
649 
650                 // this is a data line, build an entry from the extracted fields
651                 final int year  = Integer.parseInt(fields[0]);
652                 final int month = Integer.parseInt(fields[1]);
653                 final int day   = Integer.parseInt(fields[2]);
654                 final int mjd   = Integer.parseInt(fields[3]);
655                 final DateComponents dc = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd);
656                 if ((dc.getYear() % 100) != (year % 100) ||
657                     dc.getMonth() != month ||
658                     dc.getDay() != day) {
659                     throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
660                                               fileName, year, month, day, mjd);
661                 }
662                 mjdMin = FastMath.min(mjdMin, mjd);
663                 mjdMax = FastMath.max(mjdMax, mjd);
664 
665                 eop.add(new EOPEntry(mjd,
666                                      Double.parseDouble(fields[6]), Double.NaN,
667                                      UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[4])),
668                                      UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[5])),
669                                      Double.NaN, Double.NaN,
670                                      0.0, 0.0, 0.0, 0.0,
671                                      getItrfVersionProvider().getConfiguration(fileName, mjd).getVersion(),
672                                      AbsoluteDate.createMJDDate(mjd, 0, getUtc()), eopDataType,
673                                      origin, DUMMY_ORIGIN, DUMMY_ORIGIN));
674 
675             }
676 
677             if (!eop.isEmpty()) {
678                 // we have read something
679                 return eop;
680             }
681 
682             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
683                                       fileName, lineNumber);
684 
685         }
686 
687         /** Read EOP data.
688          * @param section section to parse
689          * @param isNonRotatingOrigin if true, the file contain Non-Rotating Origin nutation corrections
690          * @param reader reader from where file content is obtained
691          * @param eopDataType EOP data type
692          * @return parsed incomplete EOP
693          * @exception IOException if data can't be read
694          */
695         private List<EOPEntry> loadPoleOffsets(final Section section, final boolean isNonRotatingOrigin,
696                                                final BufferedReader reader, final EopDataType eopDataType)
697             throws IOException {
698 
699             final List<EOPEntry> eop = new ArrayList<>();
700             for (line = reader.readLine(); line != null; line = reader.readLine()) {
701 
702                 lineNumber++;
703                 final String[] fields = section.getFields(line);
704                 if (fields == null) {
705                     if (eop.isEmpty()) {
706                         // we are still waiting for the first data line
707                         continue;
708                     } else {
709                         // we have reach a separation line with next section
710                         break;
711                     }
712                 }
713 
714                 final int mjd = Integer.parseInt(fields[0]);
715                 mjdMin = FastMath.min(mjdMin, mjd);
716                 mjdMax = FastMath.max(mjdMax, mjd);
717 
718                 if (isNonRotatingOrigin) {
719                     eop.add(new EOPEntry(mjd,
720                                          Double.NaN, Double.NaN,
721                                          Double.NaN, Double.NaN,
722                                          Double.NaN, Double.NaN,
723                                          Double.NaN, Double.NaN,
724                                          UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[1])),
725                                          UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[2])),
726                                          getItrfVersionProvider().getConfiguration(fileName, mjd).getVersion(),
727                                          AbsoluteDate.createMJDDate(mjd, 0, getUtc()), eopDataType,
728                                          DUMMY_ORIGIN, DUMMY_ORIGIN, origin));
729                 } else {
730                     eop.add(new EOPEntry(mjd,
731                                          Double.NaN, Double.NaN,
732                                          Double.NaN, Double.NaN,
733                                          Double.NaN, Double.NaN,
734                                          UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[1])),
735                                          UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[2])),
736                                          Double.NaN, Double.NaN,
737                                          getItrfVersionProvider().getConfiguration(fileName, mjd).getVersion(),
738                                          AbsoluteDate.createMJDDate(mjd, 0, getUtc()), eopDataType,
739                                          DUMMY_ORIGIN, origin, DUMMY_ORIGIN));
740                 }
741             }
742 
743             if (!eop.isEmpty()) {
744                 // we have read something
745                 return eop;
746             }
747 
748             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
749                                       fileName, lineNumber);
750 
751         }
752 
753     }
754 
755 }