/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* 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 should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.foundationdb.server.types.aksql.aktypes;
import com.foundationdb.server.error.AkibanInternalException;
import com.foundationdb.server.types.Attribute;
import com.foundationdb.server.types.IllegalNameException;
import com.foundationdb.server.types.TBundleID;
import com.foundationdb.server.types.TClass;
import com.foundationdb.server.types.TClassBase;
import com.foundationdb.server.types.TClassFormatter;
import com.foundationdb.server.types.TExecutionContext;
import com.foundationdb.server.types.TInstance;
import com.foundationdb.server.types.TParser;
import com.foundationdb.server.types.FormatOptions;
import com.foundationdb.server.types.aksql.AkBundle;
import com.foundationdb.server.types.aksql.AkCategory;
import com.foundationdb.server.types.value.UnderlyingType;
import com.foundationdb.server.types.value.ValueSource;
import com.foundationdb.server.types.value.ValueTarget;
import com.foundationdb.sql.types.DataTypeDescriptor;
import com.foundationdb.sql.types.TypeId;
import com.foundationdb.util.AkibanAppender;
import com.google.common.math.LongMath;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AkInterval extends TClassBase {
public TClass widestComparable()
{
return this;
}
private static TClassFormatter monthsFormatter = new TClassFormatter() {
@Override
public void format(TInstance type, ValueSource source, AkibanAppender out) {
long months = source.getInt64();
boolean negative = false;
if(months < 0) {
negative = true;
months = -months;
}
long years = months / 12;
months -= (years * 12);
Formatter formatter = new Formatter(out.getAppendable());
if(negative)
formatter.format("INTERVAL '-%d-%d'", years, months);
else
formatter.format("INTERVAL '%d-%d'", years, months);
}
@Override
public void formatAsLiteral(TInstance type, ValueSource source, AkibanAppender out) {
long value = source.getInt64();
Formatter formatter = new Formatter(out.getAppendable());
out.append("INTERVAL '");
long years, months;
if (value < 0) {
out.append('-');
months = -value;
}
else {
months = value;
}
years = months / 12;
months -= years * 12;
String hi = null, lo = null;
if (years > 0) {
formatter.format("%d", years);
hi = lo = "YEAR";
}
if ((months > 0) || (hi == null)) {
if (hi != null) {
formatter.format("-%02d", months);
}
else {
formatter.format("%d", months);
}
lo = "MONTH";
if (hi == null) hi = lo;
}
out.append("' ");
out.append(hi);
if (hi != lo) {
out.append(" TO ");
out.append(lo);
}
}
@Override
public void formatAsJson(TInstance type, ValueSource source, AkibanAppender out, FormatOptions options) {
long months = source.getInt64();
out.append(Long.toString(months));
}
};
private static TClassFormatter secondsFormatter = new TClassFormatter() {
@Override
public void format(TInstance type, ValueSource source, AkibanAppender out) {
boolean negative = false;
long micros = secondsIntervalAs(source, TimeUnit.MICROSECONDS);
if(micros < 0) {
negative = true;
micros = -micros;
}
long days = secondsIntervalAs(micros, TimeUnit.DAYS);
micros -= TimeUnit.DAYS.toMicros(days);
long hours = secondsIntervalAs(micros, TimeUnit.HOURS);
micros -= TimeUnit.HOURS.toMicros(hours);
long minutes = secondsIntervalAs(micros, TimeUnit.MINUTES);
micros -= TimeUnit.MINUTES.toMicros(minutes);
long seconds = secondsIntervalAs(micros, TimeUnit.SECONDS);
micros -= TimeUnit.SECONDS.toMicros(seconds);
Formatter formatter = new Formatter(out.getAppendable());
if(negative)
formatter.format("INTERVAL '-%d %d:%d:%d.%05d'", days, hours, minutes, seconds, micros);
else
formatter.format("INTERVAL '%d %d:%d:%d.%05d'", days, hours, minutes, seconds, micros);
}
@Override
public void formatAsLiteral(TInstance type, ValueSource source, AkibanAppender out) {
long value = secondsIntervalAs(source, TimeUnit.MICROSECONDS);
Formatter formatter = new Formatter(out.getAppendable());
out.append("INTERVAL '");
long days, hours, mins, secs, micros;
if (value < 0) {
out.append('-');
micros = -value;
}
else {
micros = value;
}
// Could be data-driven, but just enough special cases that
// that would be pretty complicated.
secs = micros / 1000000;
micros -= secs * 1000000;
mins = secs / 60;
secs -= mins * 60;
hours = mins / 60;
mins -= hours * 60;
days = hours / 24;
hours -= days * 24;
String hi = null, lo = null;
if (days > 0) {
formatter.format("%d", days);
hi = lo = "DAY";
}
if ((hours > 0) ||
((hi != null) && ((mins > 0) || (secs > 0) || (micros > 0)))) {
if (hi != null) {
formatter.format(":%02d", hours);
}
else {
formatter.format("%d", hours);
}
lo = "HOUR";
if (hi == null) hi = lo;
}
if ((mins > 0) ||
((hi != null) && ((secs > 0) || (micros > 0)))) {
if (hi != null) {
formatter.format(":%02d", mins);
}
else {
formatter.format("%d", mins);
}
lo = "MINUTE";
if (hi == null) hi = lo;
}
if ((secs > 0) || (hi == null) || (micros > 0)) {
if (hi != null) {
formatter.format(":%02d", secs);
}
else {
formatter.format("%d", secs);
}
lo = "SECOND";
if (hi == null) hi = lo;
}
if (micros > 0) {
if ((micros % 1000) == 0)
formatter.format(".%03d", micros / 1000);
else
formatter.format(".%06d", micros);
}
out.append("' ");
out.append(hi);
if (hi != lo) {
out.append(" TO ");
out.append(lo);
}
}
@Override
public void formatAsJson(TInstance type, ValueSource source, AkibanAppender out, FormatOptions options) {
long value = secondsIntervalAs(source, TimeUnit.MICROSECONDS);
long secs = value / 1000000;
long micros = value % 1000000;
Formatter formatter = new Formatter(out.getAppendable());
formatter.format("%d.%06d", secs, micros);
}
};
/**
* A MONTHS interval, whose 64-bit value represents number of months.
*/
public static final AkInterval MONTHS = new AkInterval(
AkBundle.INSTANCE.id(),
"interval months",
AkCategory.DATE_TIME,
MonthsAttrs.class,
monthsFormatter,
1,
1,
8,
UnderlyingType.INT_64,
MonthsAttrs.FORMAT,
AkIntervalMonthsFormat.values());
/**
* <p>A SECONDS interval, whose value 64-bit value does <em>not</em> necessarily represent number of seconds.
* In fact, it almost definitely is not number of seconds; instead, the value is in some private unit. That said,
* it will still be a unit of time, so you can work with it intuitively. For instance, adding two values of
* the raw form will result in a number in the same unit that represents the sum of the two durations, and
* multiply the raw form by some number K will result in a duration K times as long as the original. The value
* 0 represents no time. In short, you can think of the unit as "Fooseconds", where Foo might be micro, nano,
* or something else but similar.</p>
*
* <p>To get values of this TClass in a meaningful way, you should use one of the {@linkplain #secondsIntervalAs}
* overloads, specifying the units you want. Units will truncate (not round) their values, as is standard in the
* JDK's TimeUnit implementation.</p>
*
* <p>If you have a value in some format, and want to convert it to the SECONDS raw format, use
* {@linkplain #secondsRawFrom(long, TimeUnit)} or {@linkplain #secondsRawFromFractionalSeconds(long)}.
* The resulting value can be added to other raw SECONDS values intuitively, as explained above.</p>
*/
public static final AkInterval SECONDS = new AkInterval(
AkBundle.INSTANCE.id(),
"interval seconds",
AkCategory.DATE_TIME,
SecondsAttrs.class,
secondsFormatter,
1,
1,
8,
UnderlyingType.INT_64,
SecondsAttrs.FORMAT,
AkIntervalSecondsFormat.values()
);
/**
* Gets the interval from a source, which should correspond to an AkInterval.SECONDS value, in some unit.
* @param source the source
* @param as the desired unit
* @return the source's value in the requested unit
*/
public static long secondsIntervalAs(ValueSource source, TimeUnit as) {
return secondsIntervalAs(source.getInt64(), as);
}
/**
* Gets the interval from a raw long, which should correspond to an AkInterval.SECONDS value, in some unit
* @param secondsIntervalRaw the raw form of the seconds value
* @param as the desired unit
* @return the raw value, translated to the requested unit
*/
public static long secondsIntervalAs(long secondsIntervalRaw, TimeUnit as) {
return as.convert(secondsIntervalRaw, AkIntervalSecondsFormat.UNDERLYING_UNIT);
}
/**
* Gets a raw SECONDS value from an interval specified in some unit.
* @param source the interval to translate to the raw form
* @param sourceUnit the incoming interval's unit
* @return the raw form
*/
public static long secondsRawFrom(long source, TimeUnit sourceUnit) {
return AkIntervalSecondsFormat.UNDERLYING_UNIT.convert(source, sourceUnit);
}
/**
* <p>Gets the raw SECONDS value from a number that represents fractions of a second. For instance, 1 would
* represent a tenth of a second; 123 would represent 123 milliseconds, etc. Values representing a greater
* precision than the raw form supports will be truncated. The raw form won't be more precise than nanoseconds.</p>
*
* <p>Negative values are fine and are interpreted as if the negative sign were in front of the whole number.</p>
*
* <p>Examples:
* <ul>
* <li>123 represents 123 milliseconds, and corresponds to 0.123 seconds.</li>
* <li>-4 represents 4 tenths of a second in the past, and corresponds to -0.4 seconds.</li>
* <li>123456789444 represents 123456789 nanoseconds, since the trailing 444 are past nanosecond resolution
* (and are thus sure to be truncated)</li>
* </ul>
* </p>
* @param source the fractional component of time, as explained above
* @return the raw form
*/
public static long secondsRawFromFractionalSeconds(long source) {
// We'll normalize this to nanoseconds, and then convert those nanos to the underlying unit. This may be
// slightly inefficient, but it keeps a nice separation of concerns. The JDK's TimeUnit doesn't go further
// than nanos, so we don't need to, either.
int numberOfDigits = 0;
for (long tmp = source; tmp != 0; tmp /= 10)
++numberOfDigits;
final int GOAL = 9;
int tooManyDigits = numberOfDigits - GOAL;
if (tooManyDigits > 0) {
// need to truncate
while (tooManyDigits-- > 0)
source /= 10;
}
if (tooManyDigits < 0) {
// need to multiply, so that 1 becomes 100000000
while (tooManyDigits++ < 0)
source *= 10;
}
// source is now in nanos.
return secondsRawFrom(source, TimeUnit.NANOSECONDS);
}
private static enum SecondsAttrs implements Attribute {
FORMAT
}
private static enum MonthsAttrs implements Attribute {
FORMAT
}
@Override
public boolean attributeIsPhysical(int attributeIndex) {
return false;
}
@Override
protected boolean attributeAlwaysDisplayed(int attributeIndex) {
return false;
}
@Override
public void attributeToString(int attributeIndex, long value, StringBuilder output) {
if (attributeIndex == formatAttribute.ordinal())
attributeToString(formatters, value, output);
else
super.attributeToString(attributeIndex, value, output);
}
@Override
protected TInstance doPickInstance(TInstance left, TInstance right, boolean suggestedNullability) {
return instance(suggestedNullability);
}
@Override
public TInstance instance(boolean nullable) {
return instance(formatAttribute.ordinal(), nullable);
}
@Override
protected void validate(TInstance type) {
int formatId = type.attribute(formatAttribute);
if ( (formatId < 0) || (formatId >= formatters.length) )
throw new IllegalNameException("unrecognized literal format ID: " + formatId);
}
@Override
public int jdbcType() {
return Types.OTHER;
}
@Override
protected DataTypeDescriptor dataTypeDescriptor(TInstance type) {
Boolean isNullable = type.nullability(); // on separate line to make NPE easier to catch
int literalFormatId = type.attribute(formatAttribute);
IntervalFormat format = formatters[literalFormatId];
TypeId typeId = format.getTypeId();
return new DataTypeDescriptor(typeId, isNullable);
}
public TInstance typeFrom(DataTypeDescriptor type) {
TypeId typeId = type.getTypeId();
IntervalFormat format = typeIdToFormat.get(typeId);
if (format == null)
throw new IllegalArgumentException("couldn't convert " + type + " to " + name());
return instance(format.ordinal(), type.isNullable());
}
private <A extends Enum<A> & Attribute> AkInterval(TBundleID bundle, String name,
Enum<?> category, Class<A> enumClass,
TClassFormatter formatter,
int internalRepVersion, int sVersion, int sSize,
UnderlyingType underlyingType,
A formatAttribute,
IntervalFormat[] formatters)
{
super(bundle, name, category, enumClass, formatter, internalRepVersion, sVersion, sSize, underlyingType,
createParser(formatAttribute, formatters), 128); // varchar len is arbitrary; I don't expect to use it
this.formatters = formatters;
this.formatAttribute = formatAttribute;
this.typeIdToFormat = createTypeIdToFormatMap(formatters);
}
public boolean isDate(TInstance ins)
{
if (ins.typeClass() instanceof AkInterval)
return formatters[0] instanceof AkIntervalMonthsFormat
|| formatters[ins.attribute(formatAttribute)] == AkIntervalSecondsFormat.DAY;
else
return false;
}
public boolean isTime(TInstance ins)
{
if (ins.typeClass() instanceof AkInterval)
return !isDate(ins);
else
return false;
}
private static void attributeToString(IntervalFormat[] formatters, long arrayIndex, StringBuilder output) {
if ( (formatters == null) || (arrayIndex < 0) || arrayIndex >= formatters.length)
output.append(arrayIndex);
else
output.append(formatters[(int)arrayIndex]);
}
private final IntervalFormat[] formatters;
private final Attribute formatAttribute;
private final Map<TypeId,IntervalFormat> typeIdToFormat;
interface IntervalFormat {
long parse(String string);
TypeId getTypeId();
int ordinal();
}
private static <F extends IntervalFormat> TParser createParser(final Attribute formatAttribute,
final F[] formatters)
{
return new TParser() {
@Override
public void parse(TExecutionContext context, ValueSource in, ValueTarget out) {
TInstance instance = context.outputType();
int literalFormatId = instance.attribute(formatAttribute);
F format = formatters[literalFormatId];
String inString = in.getString();
long months = format.parse(inString);
out.putInt64(months);
}
};
}
private static <F extends IntervalFormat> Map<TypeId, F> createTypeIdToFormatMap(F[] values) {
Map<TypeId, F> map = new HashMap<>(values.length);
for (F literalFormat : values)
map.put(literalFormat.getTypeId(), literalFormat);
return map;
}
static enum AkIntervalMonthsFormat implements IntervalFormat {
YEAR("Y+", TypeId.INTERVAL_YEAR_ID),
MONTH("M+", TypeId.INTERVAL_MONTH_ID),
YEAR_MONTH("Y+-M?", TypeId.INTERVAL_YEAR_MONTH_ID)
;
@Override
public TypeId getTypeId() {
return typeId;
}
@Override
public long parse(String string) {
return parser.parse(string);
}
AkIntervalMonthsFormat(String pattern, TypeId typeId) {
this.parser = new MonthsParser(this, pattern);
this.typeId = typeId;
}
private final AkIntervalParser<?> parser;
private final TypeId typeId;
private static class MonthsParser extends AkIntervalParser<Boolean> {
private MonthsParser(Enum<?> onBehalfOf, String pattern) {
super(onBehalfOf, pattern);
}
@Override
protected boolean buildChar(char c, boolean checkBounds, ParseCompilation<? super Boolean> result) {
switch (c) {
case 'Y':
result.addUnit(Boolean.TRUE, -1, checkBounds);
break;
case 'M':
result.addUnit(Boolean.FALSE, 12, checkBounds);
break;
case '-':
return true;
default:
throw new IllegalArgumentException("illegal pattern: " + result.inputPattern());
}
return false;
}
@Override
protected long parseLong(long parsed, Boolean isYear) {
if (isYear)
parsed = LongMath.checkedMultiply(parsed, 12);
return parsed;
}
}
}
static enum AkIntervalSecondsFormat implements IntervalFormat {
DAY("D+", TypeId.INTERVAL_DAY_ID),
HOUR("H+", TypeId.INTERVAL_HOUR_ID),
MINUTE("M+", TypeId.INTERVAL_MINUTE_ID),
SECOND("S+u", TypeId.INTERVAL_SECOND_ID, true),
DAY_HOUR("D+ H+", TypeId.INTERVAL_DAY_HOUR_ID),
DAY_MINUTE("D+ H?:M?", TypeId.INTERVAL_DAY_MINUTE_ID),
DAY_SECOND("D+ H?:M?:S?u", TypeId.INTERVAL_DAY_SECOND_ID),
HOUR_MINUTE("H+:M?", TypeId.INTERVAL_HOUR_MINUTE_ID),
HOUR_SECOND("H+:M?:S?u", TypeId.INTERVAL_HOUR_SECOND_ID),
MINUTE_SECOND("M+:S?u", TypeId.INTERVAL_MINUTE_SECOND_ID)
;
static TimeUnit UNDERLYING_UNIT = TimeUnit.MICROSECONDS;
@Override
public TypeId getTypeId() {
return typeId;
}
@Override
public long parse(String string) {
return parser.parse(string);
}
AkIntervalSecondsFormat(String pattern, TypeId typeId) {
this(pattern, typeId, false);
}
AkIntervalSecondsFormat(String pattern, TypeId typeId, boolean needsLeadingZero) {
this.parser = new SecondsParser(this, pattern, needsLeadingZero);
this.typeId = typeId;
}
private final AkIntervalParser<?> parser;
private final TypeId typeId;
private static class SecondsParser extends AkIntervalParser<TimeUnit> {
private SecondsParser(Enum<?> onBehalfOf, String pattern, boolean needsLeadingZero) {
super(onBehalfOf, pattern);
this.needsLeadingZero = needsLeadingZero;
}
@Override
protected String preParse(String string) {
return (needsLeadingZero && (string.charAt(0) == '.'))
? '0' + string
: string;
}
@Override
protected boolean buildChar(char c, boolean checkBOunds, ParseCompilation<? super TimeUnit> result) {
switch (c) {
case 'D':
result.addUnit(TimeUnit.DAYS, 31, checkBOunds);
break;
case 'H':
result.addUnit(TimeUnit.HOURS, 32, checkBOunds);
break;
case 'M':
result.addUnit(TimeUnit.MINUTES, 59, checkBOunds);
break;
case 'S':
result.addUnit(TimeUnit.SECONDS, 59, checkBOunds);
break;
case 'u':
result.addUnit(null, -1, checkBOunds); // fractional component
break;
case ' ':
case ':':
return true;
default:
throw new IllegalArgumentException("illegal pattern: " + result.inputPattern());
}
return false;
}
@Override
protected String preParseSegment(String string, TimeUnit unit) {
if (string == null)
return "0"; // inefficient because we'll just parse this, but oh well
if ( (unit == null) && (string.length() > 8) )
string = string.substring(0, 9);
return string;
}
@Override
protected long parseLong(long parsedLong, TimeUnit parsedUnit) {
if (parsedUnit != null) {
return UNDERLYING_UNIT.convert(parsedLong, parsedUnit);
}
else {
// Fractional seconds component. Need to be careful about how many digits were given.
// We'll normalize to nanoseconds, then convert to what we need. This isn't the most efficient,
// but it means we can change the underlying scale without having to remember this code.
// It's just a couple multiplications and one division, anyway.
return secondsRawFromFractionalSeconds(parsedLong);
}
}
private final boolean needsLeadingZero;
}
}
/**
* A simple parser. The rules are:
* <ul>
* <li>capital letters are special and correspond to numerical digits. If you have a capital letter followed
* by a '+', it means one or more digits, and the number's bounds shouldn't be checked. If it's followed by
* a '?', it means one or two digits, and the number's bounds should be checked. Otherwise, however many
* of the same character are in a row, that's how many digits are required (no more, no less). For instance,
* <tt>Y+ M? DD</tt> means any number of year digits (and the number can be as big as we want), followed by
* 1 or 2 month digits (and the number's bounds will be checked), followed by exactly two days digits (and
* the number's bounds will be checked). The bounds come from #buildChar</li>
* <li></li>
* <li>a lowercase 'u' means a fractional component</li>
* <li>all other letters are non-special</li>
* </ul>
* @param <U>
*/
static abstract class AkIntervalParser<U> {
@SuppressWarnings("unchecked")
public long parse(String string) {
// string could be a floating-point number
if (units.length == 1)
{
try
{
double val = Double.parseDouble(string);
return parseLong(Math.round(val), (U)units[0]);
}
catch (NumberFormatException e)
{
// does nothing.
// Move on to the next step
}
}
boolean isNegative = (string.charAt(0) == '-');
if (isNegative)
string = string.substring(1);
string = preParse(string);
Matcher matcher = regex.matcher(string);
if (!matcher.matches())
throw new AkibanInternalException("couldn't parse string as " + onBehalfOf.name() + ": " + string);
long result = 0;
for (int i = 0, len = matcher.groupCount(); i < len; ++i) {
String group = matcher.group(i+1);
@SuppressWarnings("unchecked")
U unit = (U) units[i];
String preparsedGroup = preParseSegment(group, unit);
Long longValue = Long.parseLong(preparsedGroup);
int max = maxes[i];
if (longValue > max)
throw new AkibanInternalException("out of range: " + group + " while parsing " + onBehalfOf);
long parsed = parseLong(longValue, unit);
result = LongMath.checkedAdd(result, parsed);
}
return isNegative ? -result : result;
}
protected abstract boolean buildChar(char c, boolean checkBounds, ParseCompilation<? super U> result);
protected abstract long parseLong(long value, U unit);
protected String preParse(String string) {
return string;
}
protected String preParseSegment(String string, U unit) {
return string;
}
protected AkIntervalParser(Enum<?> onBehalfOf, String pattern) {
ParseCompilation<U> built = compile(pattern);
this.regex = Pattern.compile(built.patternBuilder.toString());
this.units = built.unitsList.toArray();
this.onBehalfOf = onBehalfOf;
int maxesSize = built.maxes.size();
this.maxes = new int[maxesSize];
for (int i = 0; i < maxesSize; ++i) {
int max = built.maxes.get(i);
this.maxes[i] = (max >= 0) ? max : Integer.MAX_VALUE;
}
}
private final Enum<?> onBehalfOf;
private final Pattern regex;
private final Object[] units;
private final int[] maxes;
private static final int WILD_PLUS = -1;
private static final int WILD_QUESTION = -2;
private ParseCompilation<U> compile(String pattern) {
ParseCompilation<U> result = new ParseCompilation<>(pattern);
for (int i = 0, len = pattern.length(); i < len; ++i) {
boolean checkBounds = true;
char c = pattern.charAt(i);
if (c == 'u') {
result.patternBuilder.append("(?:\\.(\\d+))?");
}
else if (Character.isUpperCase(c)) {
int count;
int lookahead = i + 1;
if (lookahead == len) {
count = 1;
}
else if (pattern.charAt(lookahead) == '+') {
count = WILD_PLUS;
}
else if (pattern.charAt(lookahead) == '?') {
count = WILD_QUESTION;
}
else {
for(; lookahead < len; ++lookahead) {
if (pattern.charAt(lookahead) != c)
break;
}
count = lookahead - i;
}
switch (count) {
case WILD_PLUS:
result.patternBuilder.append("(\\d+)");
++i;
checkBounds = false;
break;
case WILD_QUESTION:
result.patternBuilder.append("(\\d{1,2})");
++i;
break;
default:
assert count > 0 : count;
result.patternBuilder.append("(\\d{").append(count).append("})");
i += (count-1);
break;
}
}
if (buildChar(c, checkBounds, result))
result.patternBuilder.append(c);
}
return result;
}
static class ParseCompilation<U> {
public void addUnit(U unit, int max, boolean checkBounds) {
unitsList.add(unit);
maxes.add(checkBounds ? max : -1);
}
public String inputPattern() {
return inputPattern;
}
ParseCompilation(String inputPattern) {
this.inputPattern = inputPattern;
}
private String inputPattern;
private StringBuilder patternBuilder = new StringBuilder();
private List<U> unitsList = new ArrayList<>();
private List<Integer> maxes = new ArrayList<>();
}
}
}