ParsedDateFormat.java

/*
** Module   : ParsedDateFormat.java
** Abstract : parsed form of the date format string
**
** Copyright (c) 2015-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 GES 20150115 Shared support for date format parsing and parse results access.
** 002 OM  20200616 Extracted static factory to allow better validation control.
** 003 VVT 20210317 Updated for #4880: Format parsing is re-written, no DisplayFormatParsingException
**                  exist in the system anymore, proper numbered errors are used instead.
**     VVT 20210322 Fixed after the review. See #4880-107.
** 004 VVT 20230918 The result of compilation for the 99999999 format
**                  must depend on current date component order settings. See #7515-17.
** 005 OM  20240508 Runtime parsing of date literal takes into account the current date-format.
**                  Avoid wrapping String literals, when possible.
*/

/*
** 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;

/**
 * This centralizes 4GL date format parsing and provides public access to the parse results as
 * simple member fields.
 */
public class ParsedDateFormat
{
   /** Separator character between the first two date components. */
   public final char sep1;

   /** Separator character between the last two date components. */
   public final char sep2;

   /** Size of each date component. */
   public final int[] size;

   /**
    * Construct an instance from the components.
    * 
    * @param size
    *        the array of 3 elements with sizes of date format components
    * @param sep1
    *        the value of the separator between the first and second format components or '\0' if the
    *        format string has no separators.
    * @param sep2
    *        the value of the separator between the second and third format components or '\0' if the
    *        format string has no second separator.
    */
   private ParsedDateFormat(final int[] size, final char sep1, final char sep2)
   {
      this.size = size;
      this.sep1 = sep1;
      this.sep2 = sep2;
   }

   /**
    * Test if the format has separators.
    * 
    * @return see above
    */
   public final boolean hasSep()
   {
      return sep1 != '\0' && sep2 != '\0';
   }

   /**
    * Parse the given format and use the parse results to create a new instance. If the format requested is
    * invalid (not parsable), depending on the {@code throwExceptionOnError} an appropriate exception is
    * thrown or {@code null} is returned to allow the caller to take whatever action it considers necessary.
    * This method will never return an invalid object.
    *
    * @param   fmt
    *          The 4GL date format string to parse.
    * @return  A {@code ParsedDateFormat} for the {@code fmt} requested, or {@code null} if that is invalid.
    * 
    * @throws ErrorConditionException
    *         if the format is invalid
    */
   public final static ParsedDateFormat getInstance(final String fmt)
   throws ErrorConditionException
   {
      // special cases
      switch (fmt)
      {
         case "999999":
            return new ParsedDateFormat(new int[] { 2, 2, 2 }, '\0', '\0');
         
         case "99999999":
            final String dateOrder = date.getDateOrderNative();
            final int[] sizes = new int[3];
            for(int i = 0; i < 3; i++)
            {
               final int size = dateOrder.charAt(i) == 'y' ? 4 : 2;
               sizes[i] = size;
            }
            
            return new ParsedDateFormat(sizes, '\0', '\0');

         default:
      }

      final int len = fmt.length();

      int idx = 0;

      // The first format part may be empty
      for (; idx < len && fmt.charAt(idx) == '9'; idx++)
         ;
      if (idx == len)
      {
         genDateFormatIncomleteError(fmt);
         return null;
      }

      final char sep1;
      final int size1;
      char c = fmt.charAt(idx);
      switch (c)
      {
         case '.':
         case '-':
         case '/':
            sep1 = c;
            size1 = idx++;
            break;
         default:
            ErrorManager.genInvalidCharError(idx, fmt);
            return null;
      }

      int sectionStart = idx;
      for (; idx < len && fmt.charAt(idx) == '9'; idx++)
         ;
      if (idx == len)
      {
         genDateFormatIncomleteError(fmt);
         return null;
      }

      if (idx == sectionStart)
      {
         ErrorManager.genInvalidCharError(idx, fmt);
         return null;
      }

      final int size2 = idx - sectionStart;
      final char sep2;
      c = fmt.charAt(idx);
      switch (c)
      {
         case '.':
         case '-':
         case '/':
            sep2 = c;
            idx++;
            break;
         default:
            ErrorManager.genInvalidCharError(idx, fmt);
            return null;
      }

      sectionStart = idx;
      for (; idx < len && fmt.charAt(idx) == '9'; idx++)
         ;

      // We must check these two error conditions in this very order to match the OE error handling.
      if (idx < len)
      {
         ErrorManager.genInvalidCharError(idx, fmt);
         return null;
      }
      final int size3 = idx - sectionStart;
      if (size3 == 0)
      {
         genDateFormatIncomleteError(fmt);
         return null;
      }

      return new ParsedDateFormat(new int[] { size1, size2, size3 }, sep1, sep2);
   }

   /**
    * Record or throw the error number 154.
    * 
    * @param fmt
    *        the problematic format string
    */
   private static final void genDateFormatIncomleteError(final String fmt)
   {
      ErrorManager.recordOrThrowError(154, "Date format " + fmt + " is incomplete");
   }
}