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);
}
}