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 [<,]*, 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));
}
}