# DatetimeFormatParser.java

    /*
    ** Module   : DatetimeFormatParser.java
    ** Abstract : Base class for Progress datetime/datetime-tz format parsers/formatters.
    **
    ** Copyright (c) 2021, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description---------------------------------
    ** 001 VVT 20210426 Created initial version.
    **     VVT 20210624 Javadoc fixed
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */
    package com.goldencode.p2j.util;

    /**
     * Base class for Progress datetime/datetime-tz format parsers/formatters.
     *
     * The {@link #parse(String)} method is responsible for checking the
     * Progress datetime/datetime-tz format validity.
     *
     * Also this class provides a set of callback methods,
     * each callback is called when the corresponding format component is parsed.
     *
     * By default, all callbacks do nothing, they are expected to be re-defined in subclasses.
     */
    public class DatetimeFormatParser
    extends DateFormatParser
    {
       /**
        * The hours component of the date spec was parsed.
        *
        * @param startIdx
        *        the component start index, zero based
        * @param endIdx
        *        the component end index, zero based, exclusive. The end index is
        *        always greater than the start index.
        */
       protected void visitHours(final int startIdx, final int endIdx)
       {
          // no-op
       }

       /**
        * The minutes component of the date spec was parsed.
        *
        * @param startIdx
        *        the component start index, zero based
        * @param endIdx
        *        the component end index, zero based, exclusive. The end index is
        *        always greater than the start index.
        */
       protected void visitMinutes(final int startIdx, final int endIdx)
       {
          // no-op
       }

       /**
        * The seconds component of the date spec was parsed.
        *
        * @param startIdx
        *        the component start index, zero based
        * @param endIdx
        *        the component end index, zero based, exclusive. The end index is
        *        always greater than the start index.
        */
       protected void visitSeconds(final int startIdx, final int endIdx)
       {
          // no-op
       }

       /**
        * The milliseconds component of the date spec was parsed.
        *
        * @param startIdx
        *        the component start index, zero based
        * @param endIdx
        *        the component end index, zero based, exclusive. The end index is
        *        always greater than the start index.
        */
       protected void visitMilliseconds(final int startIdx, final int endIdx)
       {
          // no-op
       }

       /**
        * The AM component of the date spec was parsed.
        *
        * @param startIdx
        *        the component start index, zero based
        * @param endIdx
        *        the component end index, zero based, exclusive. The end index is
        *        always greater than the start index.
        */
       protected void visitAMPM(final int startIdx, final int endIdx)
       {
          // no-op
       }

       /**
        * The hours in timezone component of the date spec was parsed.
        *
        * @param startIdx
        *        the component start index, zero based
        * @param endIdx
        *        the component end index, zero based, exclusive. The end index is
        *        always greater than the start index.
        */
       protected void visitTZHours(final int startIdx, final int endIdx)
       {
          // no-op
       }

       /**
        * The minutes in timezone component of the date spec was parsed.
        *
        * @param startIdx
        *        the component start index, zero based
        * @param endIdx
        *        the component end index, zero based, exclusive. The end index is
        *        always greater than the start index.
        */
       protected void visitTZMinutes(final int startIdx, final int endIdx)
       {
          // no-op
       }

       /**
        * Parse datetime format, call callbacks while parsing.
        *
        * @throws ErrorConditionException
        *         in case the date format is invalid
        */
       @Override
       public void parse(final String format) throws ErrorConditionException
       {
          // date & time separator is now space
          final String fmt = format.replace('t', ' ').replace('T', ' ').toUpperCase();
          final int len = fmt.length();

          // split date and time parts
          final int dateTimeSepIdx = fmt.indexOf(' ');
          final boolean dateSpecOnly = dateTimeSepIdx == -1;
          int dateTimeCompOrderIndex = 0;

          final byte[] datetimeCompOrder = new byte[10];
          final byte[] dateCompOrder = date.getDateComponentOrder();
          datetimeCompOrder[dateTimeCompOrderIndex++] = dateCompOrder[0];
          datetimeCompOrder[dateTimeCompOrderIndex++] = dateCompOrder[1];
          datetimeCompOrder[dateTimeCompOrderIndex++] = dateCompOrder[2];
          for (int i = 3; i < datetimeCompOrder.length; i++)
          {
             datetimeCompOrder[i] = -1;
          }

          super.parse(dateSpecOnly ? format : fmt.substring(0, dateTimeSepIdx));
          if (dateSpecOnly)
          {
             return;
          }

          int idx = dateTimeSepIdx;

          // Skip a single allowed 't' character after the date part.
          switch (format.charAt(idx))
          {
             case 't':
             case 'T':
                idx++;
                break;
             default:
          }

          // Skip any number of whitespaces
          while (idx < len && fmt.charAt(idx) == ' ')
          {
             ++idx;
          }

          if (idx == len)
          {
             // No time spec
             return;
          }

          // Count the number of 'S' characters in the rest of the format spec.
          if (fmt.chars().filter(ch -> ch == 'S').count() > 5)
          {
             ErrorManager.recordOrThrowError(14312,
                      String.format("Too many 'S' characters in format \"%s\"", fmt));
             return;
          }

          int sectionStart = idx;
          idx = parseComponent(this::visitHours, 'H', idx, fmt, len);

          if (idx > sectionStart)
          {
             if (idx == len)
             {
                return;
             }

             sectionStart = idx;
             if (fmt.charAt(idx) == ':')
             {
                idx++;
                idx = parseComponent(this::visitMinutes, 'M', idx, fmt, len);
                if (idx > sectionStart)
                {
                   if (idx == len)
                   {
                      return;
                   }

                   sectionStart = idx;
                   if (fmt.charAt(idx) == ':')
                   {
                      idx++;
                      idx = parseComponent(this::visitSeconds, 'S', idx, fmt, len);
                      if (idx > sectionStart)
                      {
                         if (idx == len)
                         {
                            return;
                         }

                         if (fmt.charAt(idx) == '.')
                         {
                            idx++;
                            sectionStart = idx;
                            idx = parseComponent(this::visitMilliseconds, 'S', idx, fmt, len);
                            if (idx > sectionStart)
                            {
                               if (idx == len)
                               {
                                  return;
                               }
                            }
                         }
                      }
                   }
                }
             }

             // Skip any number of whitespaces
             while (idx < len && fmt.charAt(idx) == ' ')
             {
                ++idx;
             }

             if (idx == len)
             {
                return;
             }

             if (fmt.charAt(idx) == 'A')
             {
                sectionStart = idx;
                idx++;
                if (idx < len && fmt.charAt(idx) == 'M')
                {
                   idx++;
                }
                visitAMPM(sectionStart, idx);
             }
          }

          if (idx == len)
          {
             return;
          }

          // Parse optional TZ part
          switch (fmt.charAt(idx))
          {
             case '+':
             case '-':
                idx++;
                sectionStart = idx;
                int k = idx;
                for (; k < len && fmt.charAt(k) == 'H'; k++)
                   ;

                if (k > sectionStart)
                {
                   // Note: the leading +/- character belongs to the component.
                   visitTZHours(sectionStart - 1, k);
                }
                idx = k;

                if (idx > sectionStart)
                {
                   if (idx == len)
                   {
                      return;
                   }
                   if (fmt.charAt(idx) == ':')
                   {
                      idx++;
                      sectionStart = idx;
                      idx = parseComponent(this::visitTZMinutes, 'M', idx, fmt, len);
                   }
                }
          }

          // Skip any number of trailing whitespaces
          while (idx < len && fmt.charAt(idx) == ' ')
          {
             ++idx;
          }

          if (idx < len)
          {
             ErrorManager.genInvalidCharError(idx, format);
             return;
          }
       }

       /**
        * Parse one format component.
        *
        * @param cb
        *        the callback to call
        * @param c
        *        the character to skip, in upper case
        * @param fromIdx
        *        start index
        * @param fmt
        *        format string
        * @param len
        *        format length
        * @return the resulting index
        */
       private final static int parseComponent(final Callback cb, final char c, final int fromIdx,
                final String fmt, final int len)
       {
          // Note: when parsing format, OE accepts ANY number of letter for in any part of time spec.
          int i = fromIdx;
          for (; i < len && Character.toUpperCase(fmt.charAt(i)) == c; i++)
             ;

          if (i > fromIdx)
          {
             cb.call(fromIdx, i);
          }

          return i;
       }

       /**
        * Helper interface used to describe for all component callbacks
        *
        */
       private static interface Callback
       {
          void call(final int startIdx, final int endIdx);
       }

    }