EopC04FilesLoader.java

  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.frames;

  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.StandardCharsets;
  23. import java.util.ArrayList;
  24. import java.util.Collection;
  25. import java.util.List;
  26. import java.util.SortedSet;
  27. import java.util.function.Supplier;
  28. import java.util.regex.Matcher;
  29. import java.util.regex.Pattern;

  30. import org.orekit.data.DataProvidersManager;
  31. import org.orekit.errors.OrekitException;
  32. import org.orekit.errors.OrekitMessages;
  33. import org.orekit.time.AbsoluteDate;
  34. import org.orekit.time.DateComponents;
  35. import org.orekit.time.TimeComponents;
  36. import org.orekit.time.TimeScale;
  37. import org.orekit.utils.Constants;
  38. import org.orekit.utils.IERSConventions;
  39. import org.orekit.utils.IERSConventions.NutationCorrectionConverter;

  40. /** Loader for EOP C04 files.
  41.  * <p>EOP C04 files contain {@link EOPEntry
  42.  * Earth Orientation Parameters} consistent with ITRF20xx for one year periods, with various
  43.  * xx (05, 08, 14, 20) depending on the data source.</p>
  44.  * <p>The EOP C04 files retrieved from the old ftp site
  45.  * <a href="ftp://ftp.iers.org/products/eop/long-term/">ftp://ftp.iers.org/products/eop/long-term/</a>
  46.  * were recognized thanks to their base names, which must match one of the patterns
  47.  * {@code eopc04_##_IAU2000.##} or {@code eopc04_##.##} (or the same ending with <code>.gz</code> for
  48.  * gzip-compressed files) where # stands for a digit character. As of early 2023, this ftp site
  49.  * seems not to be accessible anymore.</p>
  50.  * <p>
  51.  * The official source for these files is now the web site
  52.  * <a href="https://hpiers.obspm.fr/eoppc/eop/">https://hpiers.obspm.fr/eoppc/eop/</a>. These
  53.  * files do <em>not</em> follow the old naming convention that was used in the older ftp site.
  54.  * They lack the _05, _08 or _14 markers in the file names. The ITRF year appears only in the URL
  55.  * (with directories eopc04_05, eop04_c08…). The directory for the current data is named eopc04
  56.  * without any suffix. So before 2023-02-14 the eopc04 directory would contain files compatible with
  57.  * ITRF2014 and after 2023-02-14 it would contain files compatible with ITRF2020. In each directory,
  58.  * the files don't have any marker, hence users downloading eopc04.99 file from eopc04_05 would get
  59.  * a file compatible with ITRF2005 whereas users downloading a file with the exact same name eopc04.99
  60.  * but from eop04_c08 would get a file compatible with ITRF2008.
  61.  * </p>
  62.  * <p>
  63.  * Starting with Orekit version 12.0, the ITRF year is retrieved by analyzing the file header, it is
  64.  * not linked to file name anymore, hence it is compatible with any IERS site layout.
  65.  * </p>
  66.  * <p>
  67.  * This class is immutable and hence thread-safe
  68.  * </p>
  69.  * @author Luc Maisonobe
  70.  */
  71. class EopC04FilesLoader extends AbstractEopLoader implements EopHistoryLoader {

  72.     /** Build a loader for IERS EOP C04 files.
  73.      * @param supportedNames regular expression for supported files names
  74.      * @param manager provides access to the EOP C04 files.
  75.      * @param utcSupplier UTC time scale.
  76.      */
  77.     EopC04FilesLoader(final String supportedNames,
  78.                       final DataProvidersManager manager,
  79.                       final Supplier<TimeScale> utcSupplier) {
  80.         super(supportedNames, manager, utcSupplier);
  81.     }

  82.     /** {@inheritDoc} */
  83.     public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
  84.                             final SortedSet<EOPEntry> history) {
  85.         final Parser parser = new Parser(converter, getUtc());
  86.         final EopParserLoader loader = new EopParserLoader(parser);
  87.         this.feed(loader);
  88.         history.addAll(loader.getEop());
  89.     }

  90.     /** Internal class performing the parsing. */
  91.     static class Parser extends AbstractEopParser {

  92.         /** Simple constructor.
  93.          * @param converter converter to use
  94.          * @param utc       time scale for parsing dates.
  95.          */
  96.         Parser(final NutationCorrectionConverter converter,
  97.                final TimeScale utc) {
  98.             super(converter, null, utc);
  99.         }

  100.         /** {@inheritDoc} */
  101.         public Collection<EOPEntry> parse(final InputStream input, final String name)
  102.             throws IOException, OrekitException {

  103.             final List<EOPEntry> history = new ArrayList<>();

  104.             // set up a reader for line-oriented EOP C04 files
  105.             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
  106.                 // reset parse info to start new file (do not clear history!)
  107.                 int lineNumber   = 0;
  108.                 boolean inHeader = true;
  109.                 final LineParser[] tentativeParsers = new LineParser[] {
  110.                     new LineWithoutRatesParser(name),
  111.                     new LineWithRatesParser(name)
  112.                 };
  113.                 LineParser selectedParser = null;

  114.                 // read all file
  115.                 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  116.                     ++lineNumber;
  117.                     boolean parsed = false;

  118.                     if (inHeader) {
  119.                         // maybe it's an header line
  120.                         for (final LineParser parser : tentativeParsers) {
  121.                             if (parser.parseHeaderLine(line)) {
  122.                                 // we recognized one EOP C04 format
  123.                                 selectedParser = parser;
  124.                                 break;
  125.                             }
  126.                         }
  127.                     }

  128.                     if (selectedParser != null) {
  129.                         // maybe it's a data line
  130.                         final EOPEntry entry = selectedParser.parseDataLine(line);
  131.                         if (entry != null) {

  132.                             // this is a data line, build an entry from the extracted fields
  133.                             history.add(entry);
  134.                             parsed = true;

  135.                             // we know we have already finished header
  136.                             inHeader = false;

  137.                         }
  138.                     }

  139.                     if (!(inHeader || parsed)) {
  140.                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  141.                                 lineNumber, name, line);
  142.                     }
  143.                 }

  144.                 // check if we have read something
  145.                 if (inHeader) {
  146.                     throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
  147.                 }
  148.             }

  149.             return history;
  150.         }

  151.         /** Base parser for EOP C04 lines.
  152.          * @since 12.0
  153.          */
  154.         private abstract class LineParser {

  155.             /** Pattern for ITRF version. */
  156.             private final Pattern itrfVersionPattern;

  157.             /** Pattern for columns header. */
  158.             private final Pattern columnHeaderPattern;

  159.             /** Pattern for data lines. */
  160.             private final Pattern dataPattern;

  161.             /** Year group. */
  162.             private final int yearGroup;

  163.             /** Month group. */
  164.             private final int monthGroup;

  165.             /** Day group. */
  166.             private final int dayGroup;

  167.             /** MJD group. */
  168.             private final int mjdGroup;

  169.             /** Name of the stream for error messages. */
  170.             private final String name;

  171.             /** ITRF version. */
  172.             private ITRFVersion itrfVersion;

  173.             /** Simple constructor.
  174.              * @param itrfVersionRegexp regular expression for ITRF version
  175.              * @param columnsHeaderRegexp regular expression for columns header
  176.              * @param dataRegexp regular expression for data lines
  177.              * @param yearGroup year group
  178.              * @param monthGroup month group
  179.              * @param dayGroup day group
  180.              * @param mjdGroup MJD group
  181.              * @param name  of the stream for error messages.
  182.              */
  183.             protected LineParser(final String itrfVersionRegexp, final String columnsHeaderRegexp,
  184.                                  final String dataRegexp,
  185.                                  final int yearGroup, final int monthGroup, final int dayGroup,
  186.                                  final int mjdGroup, final String name) {
  187.                 this.itrfVersionPattern  = Pattern.compile(itrfVersionRegexp);
  188.                 this.columnHeaderPattern = Pattern.compile(columnsHeaderRegexp);
  189.                 this.dataPattern         = Pattern.compile(dataRegexp);
  190.                 this.yearGroup           = yearGroup;
  191.                 this.monthGroup          = monthGroup;
  192.                 this.dayGroup            = dayGroup;
  193.                 this.mjdGroup            = mjdGroup;
  194.                 this.name                = name;
  195.             }

  196.             /** Get the ITRF version for this EOP C04 file.
  197.              * @return ITRF version
  198.              */
  199.             protected ITRFVersion getItrfVersion() {
  200.                 return itrfVersion;
  201.             }

  202.             /** Parse a header line.
  203.              * @param line line to parse
  204.              * @return true if line was recognized (either ITRF version or columns header)
  205.              */
  206.             public boolean parseHeaderLine(final String line) {
  207.                 final Matcher itrfVersionMatcher = itrfVersionPattern.matcher(line);
  208.                 if (itrfVersionMatcher.matches()) {
  209.                     switch (Integer.parseInt(itrfVersionMatcher.group(1))) {
  210.                         case 5 :
  211.                             itrfVersion = ITRFVersion.ITRF_2005;
  212.                             break;
  213.                         case 8 :
  214.                             itrfVersion = ITRFVersion.ITRF_2008;
  215.                             break;
  216.                         case 14 :
  217.                             itrfVersion = ITRFVersion.ITRF_2014;
  218.                             break;
  219.                         case 20 :
  220.                             itrfVersion = ITRFVersion.ITRF_2020;
  221.                             break;
  222.                         default :
  223.                             throw new OrekitException(OrekitMessages.NO_SUCH_ITRF_FRAME, itrfVersionMatcher.group(1));
  224.                     }
  225.                     return true;
  226.                 } else {
  227.                     final Matcher columnHeaderMatcher = columnHeaderPattern.matcher(line);
  228.                     if (columnHeaderMatcher.matches()) {
  229.                         parseColumnsHeaderLine(columnHeaderMatcher);
  230.                         return true;
  231.                     }
  232.                     return false;
  233.                 }
  234.             }

  235.             /** Parse a data line.
  236.              * @param line line to parse
  237.              * @return EOP entry for the line, or null if line does not match expected regular expression
  238.              */
  239.             public EOPEntry parseDataLine(final String line) {

  240.                 final Matcher matcher = dataPattern.matcher(line);
  241.                 if (!matcher.matches()) {
  242.                     // this is not a data line
  243.                     return null;
  244.                 }

  245.                 // check date
  246.                 final DateComponents dc = new DateComponents(Integer.parseInt(matcher.group(yearGroup)),
  247.                                                              Integer.parseInt(matcher.group(monthGroup)),
  248.                                                              Integer.parseInt(matcher.group(dayGroup)));
  249.                 final int    mjd   = Integer.parseInt(matcher.group(mjdGroup));
  250.                 if (dc.getMJD() != mjd) {
  251.                     throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
  252.                                               name, dc.getYear(), dc.getMonth(), dc.getDay(), mjd);
  253.                 }

  254.                 return parseDataLine(matcher, dc);

  255.             }

  256.             /** Parse a columns header line.
  257.              * @param matcher matcher for line
  258.              */
  259.             protected abstract void parseColumnsHeaderLine(Matcher matcher);

  260.             /** Parse a data line.
  261.              * @param matcher matcher for line
  262.              * @param dc date components already extracted from the line
  263.              * @return EOP entry for the line
  264.              */
  265.             protected abstract EOPEntry parseDataLine(Matcher matcher, DateComponents dc);

  266.         }

  267.         /** Parser for data lines without pole rates.
  268.          * <p>
  269.          * ITRF markers have either the following form:
  270.          * </p>
  271.          * <pre>
  272.          *                           EOP (IERS) 05 C04
  273.          * </pre>
  274.          * <p>
  275.          * or the following form:
  276.          * </p>
  277.          * <pre>
  278.          *                           EOP (IERS) 14 C04 TIME SERIES
  279.          * </pre>
  280.          * <p>
  281.          * Header have either the following form:
  282.          * </p>
  283.          * <pre>
  284.          *       Date      MJD      x          y        UT1-UTC       LOD         dPsi      dEps       x Err     y Err   UT1-UTC Err  LOD Err    dPsi Err   dEpsilon Err
  285.          *                          "          "           s           s            "         "        "          "          s           s            "         "
  286.          *      (0h UTC)
  287.          * </pre>
  288.          * <p>
  289.          * or the following form:
  290.          * </p>
  291.          * <pre>
  292.          *       Date      MJD      x          y        UT1-UTC       LOD         dX        dY        x Err     y Err   UT1-UTC Err  LOD Err     dX Err       dY Err
  293.          *                          "          "           s           s          "         "           "          "          s         s            "           "
  294.          *      (0h UTC)
  295.          * </pre>
  296.          * <p>
  297.          * The data lines in the EOP C04 yearly data files have either the following fixed form:
  298.          * </p>
  299.          * <pre>
  300.          * year month day MJD …12 floating values fields in decimal format...
  301.          * 2000   1   1  51544   0.043242   0.377915   0.3554777   …
  302.          * 2000   1   2  51545   0.043515   0.377753   0.3546065   …
  303.          * 2000   1   3  51546   0.043623   0.377452   0.3538444   …
  304.          * </pre>
  305.          * @since 12.0
  306.          */
  307.         private class LineWithoutRatesParser extends LineParser {

  308.             /** Nutation header group. */
  309.             private static final int NUTATION_HEADER_GROUP = 1;

  310.             /** Year group. */
  311.             private static final int YEAR_GROUP = 1;

  312.             /** Month group. */
  313.             private static final int MONTH_GROUP = 2;

  314.             /** Day group. */
  315.             private static final int DAY_GROUP = 3;

  316.             /** MJD group. */
  317.             private static final int MJD_GROUP = 4;

  318.             /** X component of pole motion group. */
  319.             private static final int POLE_X_GROUP = 5;

  320.             /** Y component of pole motion group. */
  321.             private static final int POLE_Y_GROUP = 6;

  322.             /** UT1-UTC group. */
  323.             private static final int UT1_UTC_GROUP = 7;

  324.             /** LoD group. */
  325.             private static final int LOD_GROUP = 8;

  326.             /** Correction for nutation first field (either dX or dPsi). */
  327.             private static final int NUT_0_GROUP = 9;

  328.             /** Correction for nutation second field (either dY or dEps). */
  329.             private static final int NUT_1_GROUP = 10;

  330.             /** Indicator for non-rotating origin. */
  331.             private boolean isNonRotatingOrigin;

  332.             /** Simple constructor.
  333.              * @param name  of the stream for error messages.
  334.              */
  335.             LineWithoutRatesParser(final String name) {
  336.                 super("^ +EOP +\\(IERS\\) +([0-9][0-9]) +C04.*",
  337.                       "^ *Date +MJD +x +y +UT1-UTC +LOD +((?:dPsi +dEps)|(?:dX +dY)) .*",
  338.                       "^(\\d+) +(\\d+) +(\\d+) +(\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+)(?: +(-?\\d+\\.\\d+)){6}$",
  339.                       YEAR_GROUP, MONTH_GROUP, DAY_GROUP, MJD_GROUP,
  340.                       name);
  341.             }

  342.             /** {@inheritDoc} */
  343.             @Override
  344.             protected void parseColumnsHeaderLine(final Matcher matcher) {
  345.                 isNonRotatingOrigin = matcher.group(NUTATION_HEADER_GROUP).startsWith("dX");
  346.             }

  347.             /** {@inheritDoc} */
  348.             @Override
  349.             protected EOPEntry parseDataLine(final Matcher matcher, final DateComponents dc) {

  350.                 final AbsoluteDate date = new AbsoluteDate(dc, getUtc());

  351.                 final double x     = Double.parseDouble(matcher.group(POLE_X_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS;
  352.                 final double y     = Double.parseDouble(matcher.group(POLE_Y_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS;
  353.                 final double dtu1  = Double.parseDouble(matcher.group(UT1_UTC_GROUP));
  354.                 final double lod   = Double.parseDouble(matcher.group(LOD_GROUP));
  355.                 final double[] equinox;
  356.                 final double[] nro;
  357.                 if (isNonRotatingOrigin) {
  358.                     nro = new double[] {
  359.                         Double.parseDouble(matcher.group(NUT_0_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS,
  360.                         Double.parseDouble(matcher.group(NUT_1_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS
  361.                     };
  362.                     equinox = getConverter().toEquinox(date, nro[0], nro[1]);
  363.                 } else {
  364.                     equinox = new double[] {
  365.                         Double.parseDouble(matcher.group(NUT_0_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS,
  366.                         Double.parseDouble(matcher.group(NUT_1_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS
  367.                     };
  368.                     nro = getConverter().toNonRotating(date, equinox[0], equinox[1]);
  369.                 }

  370.                 return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, Double.NaN, Double.NaN,
  371.                                     equinox[0], equinox[1], nro[0], nro[1],
  372.                                     getItrfVersion(), date);

  373.             }
  374.         }

  375.         /** Parser for data lines with pole rates.
  376.          * <p>
  377.          * ITRF markers have either the following form:
  378.          * </p>
  379.          * <pre>
  380.          * # EOP (IERS) 20 C04 TIME SERIES  consistent with ITRF 2020 - sampled at 0h UTC
  381.          * </pre>
  382.          * <p>
  383.          * Header have either the following form:
  384.          * </p>
  385.          * <pre>
  386.          * # YR  MM  DD  HH       MJD        x(")        y(")  UT1-UTC(s)       dX(")      dY(")       xrt(")      yrt(")      LOD(s)        x Er        y Er  UT1-UTC Er      dX Er       dY Er       xrt Er      yrt Er      LOD Er
  387.          * </pre>
  388.          * <p>
  389.          * The data lines in the EOP C04 yearly data files have either the following fixed form:
  390.          * </p>
  391.          * <pre>
  392.          * year month day hour MJD (in floating format) …16 floating values fields in decimal format...
  393.          * 2015   1   1  12  57023.50    0.030148    0.281014   …
  394.          * 2015   1   2  12  57024.50    0.029219    0.281441   …
  395.          * 2015   1   3  12  57025.50    0.028777    0.281824   …
  396.          * </pre>
  397.          * @since 12.0
  398.          */
  399.         private class LineWithRatesParser extends LineParser {

  400.             /** Year group. */
  401.             private static final int YEAR_GROUP = 1;

  402.             /** Month group. */
  403.             private static final int MONTH_GROUP = 2;

  404.             /** Day group. */
  405.             private static final int DAY_GROUP = 3;

  406.             /** Hour group. */
  407.             private static final int HOUR_GROUP = 4;

  408.             /** MJD group. */
  409.             private static final int MJD_GROUP = 5;

  410.             /** X component of pole motion group. */
  411.             private static final int POLE_X_GROUP = 6;

  412.             /** Y component of pole motion group. */
  413.             private static final int POLE_Y_GROUP = 7;

  414.             /** UT1-UTC group. */
  415.             private static final int UT1_UTC_GROUP = 8;

  416.             /** Correction for nutation first field. */
  417.             private static final int NUT_DX_GROUP = 9;

  418.             /** Correction for nutation second field. */
  419.             private static final int NUT_DY_GROUP = 10;

  420.             /** X rate component of pole motion group.
  421.              * @since 12.0
  422.              */
  423.             private static final int POLE_X_RATE_GROUP = 11;

  424.             /** Y rate component of pole motion group.
  425.              * @since 12.0
  426.              */
  427.             private static final int POLE_Y_RATE_GROUP = 12;

  428.             /** LoD group. */
  429.             private static final int LOD_GROUP = 13;

  430.             /** Simple constructor.
  431.              * @param name  of the stream for error messages.
  432.              */
  433.             LineWithRatesParser(final String name) {
  434.                 super("^# +EOP +\\(IERS\\) +([0-9][0-9]) +C04.*",
  435.                       "^# +YR +MM +DD +H +MJD +x\\(\"\\) +y\\(\"\\) +UT1-UTC\\(s\\) +dX\\(\"\\) +dY\\(\"\\) +xrt\\(\"\\) +yrt\\'\"\\) +.*",
  436.                       "^(\\d+) +(\\d+) +(\\d+) +(\\d+) +(\\d+)\\.\\d+ +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+)(?: +(-?\\d+\\.\\d+)){8}$", // we intentionally ignore MJD fractional part
  437.                       YEAR_GROUP, MONTH_GROUP, DAY_GROUP, MJD_GROUP,
  438.                       name);
  439.             }

  440.             /** {@inheritDoc} */
  441.             @Override
  442.             protected void parseColumnsHeaderLine(final Matcher matcher) {
  443.                 // nothing to do here
  444.             }

  445.             /** {@inheritDoc} */
  446.             @Override
  447.             protected EOPEntry parseDataLine(final Matcher matcher, final DateComponents dc) {

  448.                 final TimeComponents tc = new TimeComponents(Integer.parseInt(matcher.group(HOUR_GROUP)), 0, 0.0);
  449.                 final AbsoluteDate date = new AbsoluteDate(dc, tc, getUtc());

  450.                 final double x     = Double.parseDouble(matcher.group(POLE_X_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS;
  451.                 final double y     = Double.parseDouble(matcher.group(POLE_Y_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS;
  452.                 final double xRate = Double.parseDouble(matcher.group(POLE_X_RATE_GROUP)) *
  453.                                      Constants.ARC_SECONDS_TO_RADIANS / Constants.JULIAN_DAY;
  454.                 final double yRate = Double.parseDouble(matcher.group(POLE_Y_RATE_GROUP)) *
  455.                                      Constants.ARC_SECONDS_TO_RADIANS / Constants.JULIAN_DAY;
  456.                 final double dtu1  = Double.parseDouble(matcher.group(UT1_UTC_GROUP));
  457.                 final double lod   = Double.parseDouble(matcher.group(LOD_GROUP));
  458.                 final double[] nro = new double[] {
  459.                     Double.parseDouble(matcher.group(NUT_DX_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS,
  460.                     Double.parseDouble(matcher.group(NUT_DY_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS
  461.                 };
  462.                 final double[] equinox = getConverter().toEquinox(date, nro[0], nro[1]);

  463.                 return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, xRate, yRate,
  464.                                     equinox[0], equinox[1], nro[0], nro[1],
  465.                                     getItrfVersion(), date);

  466.             }
  467.         }

  468.     }

  469. }