# NumberFormatParser.java

    /*
    ** Module   : NumberFormatParser.java
    ** Abstract : Base class for Progress number format parsers/formatters.
    **
    ** Copyright (c) 2021, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description---------------------------------
    ** 001 VVT 20210417 Created initial version.
    **     VVT 20210426 Format of all calbacks unified. The formatting algorithm fixed in multiple
    **                  places, and now passes all unit tests.
    */
    /*
    ** 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;

    import java.util.regex.*;

    /**
     * Base class for Progress number format parsers/formatters.
     *
     * The {@link #parse(String)} method is responsible for checking the
     * Progress date 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 NumberFormatParser
    {
       /** Pre-compiled regex pattern to right trim format string */
       private static final Pattern RIGHT_SPACES_PATTERN = Pattern.compile("\\s*$");

       /**
        * Parse number format, call callbacks while parsing.
        *
        * @param format
        *        the format string to parse
        *
        * @throws ErrorConditionException
        *         in case the number format is invalid
        */
       public void parse(final String format) throws ErrorConditionException
       {
          // Trim the format right.
          final String fmt = RIGHT_SPACES_PATTERN.matcher(format).replaceFirst("");

          // The length of format string
          final int len = fmt.length();

          // The current character read
          char c = 0;

          // The current index into format string
          int formatIdx = 0;

          // True if at least one left sign parenthesis exist
          boolean leftParenthesisExists = false;

          // Collect the optional user-specified prefix: leading
          // characters that are not valid formatting characters.
          // This string can also include any number of left parenthesizes.
          endLeft:
          for (;; formatIdx++)
          {
             if (formatIdx == len)
             {
                genNoDigitsError(fmt);
                return;
             }

             c = fmt.charAt(formatIdx);
             switch (c)
             {
                case ',':
                case '.':
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '>':
                case 'z':
                case 'Z':
                case '*':
                case '+':
                case '-':
                   // A 'digit' character found.
                   break endLeft;

                case '(':
                   leftParenthesisExists = true;
                   visitSign(formatIdx, formatIdx + 1);
                   break;

                default:
                   visitFillChar(c);
             }
          }

          // True is '+' or '-' character was parsed
          boolean plusOrMinusExists = false;

          // scan optional '+' or '-' sign
          if (!leftParenthesisExists)
          {
             switch (c)
             {
                case '+':
                case '-':
                   plusOrMinusExists = true;
                   visitSign(formatIdx, formatIdx + 1);
                   formatIdx++;
                   break;
                default:
             }
          }

          if (formatIdx == len)
          {
             genNoDigitsError(fmt);
             return;
          }

          // The character after '+' or '-' must be a 'digit'.
          if (plusOrMinusExists)
          {
             switch (fmt.charAt(formatIdx))
             {
                case '9':
                case '*':
                case ',':
                case '.':
                case '>':
                case 'z':
                case 'Z':
                   break;

                default:
                   ErrorManager.genInvalidCharError(formatIdx, fmt);
                   return;
             }
          }

          // Scan the 'digits' section

          // The start of currently parsed component
          int sectionStart = formatIdx;

          // Count the '>' characters in the left digits section,
          // this should match the number of '<' in the right digits section
          int leftBalance = 0;

          // At least one 'digit' character found flag
          boolean anyDigit = false;

          // 2. Scan optional left balance and separators portion: [>,]*, update digits and leftBalance counters
          endLeftBalance:
          for (;; formatIdx++)
          {
             if (formatIdx == len)
             {
                if (!anyDigit)
                {
                   genNoDigitsError(fmt);
                   return;
                }

                break;
             }

             c = fmt.charAt(formatIdx);
             switch (c)
             {
                case '>':
                   leftBalance++;
                   anyDigit = true;
                   break;

                case ',':
                   leftBalance++;
                   break;

                default:
                   break endLeftBalance;
             }
          }

          // 3. Scan optional left [9zZ*,]* portion
          endDigitsSection:
          for (; formatIdx < len; formatIdx++)
          {
             switch (fmt.charAt(formatIdx))
             {
                case '9':
                case 'z':
                case 'Z':
                case '*':
                   anyDigit = true;
                   break;

                case ',':
                   break;

                default:
                   break endDigitsSection;
             }
          }

          if (formatIdx > sectionStart)
          {
             visitLeftDigits(sectionStart, formatIdx);
          }

          if (formatIdx == len)
          {
             if (!anyDigit)
             {
                genNoDigitsError(fmt);
             }

             return;
          }

          // 4. Scan optional right side part
          switch (fmt.charAt(formatIdx))
          {
             case '.':
                formatIdx++;

                sectionStart = formatIdx;

                int separators = 0;
                // Scan optional digits and FORMAT_GROUP_SEP [9zZ*,]*
                endDigitsSection:
                for (; formatIdx < len; formatIdx++)
                {
                   c = fmt.charAt(formatIdx);
                   if (sectionStart == formatIdx)
                   {
                      // First digit character after '.' can only be '9'
                      switch (c)
                      {
                         case 'z':
                         case 'Z':
                         case '*':
                         case ',':
                            ErrorManager.genInvalidCharError(formatIdx, fmt);
                            return;
                         default:
                      }
                   }

                   switch (c)
                   {
                      case '9':
                      case 'z':
                      case 'Z':
                      case '*':
                         anyDigit = true;
                         break;

                      case ',':
                         separators++;
                         break;

                      default:
                         break endDigitsSection;
                   }
                }

                // Scan optional right balance [&lt,]*, check the right balance is no more than the left one.
                endRightBanance:
                for (int rightBalance = 0; formatIdx < len; formatIdx++)
                {
                   switch (fmt.charAt(formatIdx))
                   {
                      case ',':
                         separators++;
                         // $FALL-THROUGH$
                      case '<':
                         rightBalance++;

                         if (rightBalance > leftBalance)
                         {
                            ErrorManager.genInvalidCharError(formatIdx, fmt);
                            return;
                         }
                         break;
                      default:
                         break endRightBanance;
                   }
                }

                // Note: we need to visit the callback method event if no digits are in the right part.
                visitRightDigits(sectionStart, formatIdx, separators);

                break;

             case '<':
                // If there is no right part, there must be no right balance either.
                ErrorManager.genInvalidCharError(formatIdx, fmt);
                return;

             default:
          }

          if (formatIdx == len)
          {
             if (!anyDigit)
             {
                genNoDigitsError(fmt);
             }

             return;
          }

          c = fmt.charAt(formatIdx);

          if (!leftParenthesisExists)
          {
             switch (c)
             {
                case '-':
                case '+':
                   if (plusOrMinusExists || leftParenthesisExists)
                   {
                      ErrorManager.genInvalidCharError(formatIdx, fmt);
                      return;
                   }

                   visitSign(formatIdx, formatIdx + 1);
                   formatIdx++;
                   break;

                default:
                   switch (fmt.substring(formatIdx).toLowerCase())
                   {
                      // FIXME: record these in callbacks
                      case "dr":
                         visitSign(formatIdx, formatIdx + 2);
                         formatIdx += 2;
                         break;
                      case "cr":
                         visitSign(formatIdx, formatIdx + 2);
                         formatIdx += 2;
                         break;
                      case "db":
                         visitSign(formatIdx, formatIdx + 2);
                         formatIdx += 2;
                         break;
                      default:
                   }
             }
          }

          // Scan the rest of format as right user section.
          sectionStart = formatIdx;

          // Scan optional right sign
          if (leftParenthesisExists)
          {
             // If at least one left paren is present, it MUST be the right parent here.
             if (c != ')')
             {
                ErrorManager.genInvalidCharError(formatIdx, fmt);
                return;
             }

             visitSign(formatIdx, formatIdx + 1);

             // FIXME: record the right paren using a callback
             formatIdx++;
          }

          for (; formatIdx < len; formatIdx++)
          {
             c = fmt.charAt(formatIdx);
             switch (c)
             {
                case '>':
                case ',':
                case '*':
                case 'z':
                case 'Z':
                case '+':
                case '-':
                   if (formatIdx > sectionStart)
                   {
                      formatIdx--;
                   }
                   // $FALL-THROUGH$
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case ')':
                case '(':
                   ErrorManager.genInvalidCharError(formatIdx, fmt);
                   return;

                default:
                   visitFillChar(c);
             }
          }

          // There must be at least one digit in the format.
          if (!anyDigit)
          {
             genNoDigitsError(fmt);
             return;
          }
       }

       /**
        * Non-empty left digits section was parsed.
        * <p>
        * The default implementation does nothing.
        *
        * @param startIdx
        *        the section start index in format string, zero-based, inclusive
        * @param endIdx
        *        the section end index in format string, zero-based, exclusive
        */
       protected void visitLeftDigits(final int startIdx, final int endIdx)
       {
          // no-op
       }

       /**
        * Right digits section was parsed (may be empty).
        * <p>
        * The default implementation does nothing.
        *
        * @param startIdx
        *        the section start index in format string, zero-based, inclusive
        * @param endIdx
        *        the section end index in format string, zero-based, exclusive
        * @param separators
        *        the number of digit separator ',' character in right digits
        */
       protected void visitRightDigits(int startIdx, int endIdx, int separators)
       {
          // no-op
       }

       /**
        * A sign spec was parsed.
        * <p>
        * Sign specs are: '+', '-', '(', ')', DR, CR and DB.
        * <p>
        * The default implementation does nothing.
        *
        * @param startIdx
        *        the section start index in format string, zero-based, inclusive
        * @param endIdx
        *        the section end index in format string, zero-based, exclusive
        */
       protected void visitSign(final int startIdx, final int endIdx)
       {
          // no-op
       }

       /**
        * A fill character was parsed.
        * <p>
        * The default implementation does nothing.
        *
        * @param c
        *        the fill character
        */
       protected void visitFillChar(final char c)
       {
          // no-op
       }

       /**
        * Generate an error if no digits were found in a numeric format string.
        *
        * @param    fmt
        *           The incorrect format string.
        *
        * @throws   ErrorConditionException
        */
       private static void genNoDigitsError(final String fmt) throws ErrorConditionException
       {
          ErrorManager.recordOrThrowError(148,
                   String.format("Numeric format %s provides for no digits", fmt));
       }
    }