/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (NumberProcessor.java) is part of project Time4J.
*
* Time4J is free software: You can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* Time4J 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Time4J. If not, see <http://www.gnu.org/licenses/>.
* -----------------------------------------------------------------------
*/
package net.time4j.format.expert;
import net.time4j.PlainDate;
import net.time4j.engine.AttributeQuery;
import net.time4j.engine.ChronoDisplay;
import net.time4j.engine.ChronoElement;
import net.time4j.format.Attributes;
import net.time4j.format.Leniency;
import net.time4j.format.NumberSystem;
import net.time4j.format.NumericalElement;
import net.time4j.history.internal.HistorizedElement;
import java.io.IOException;
import java.util.Set;
/**
* <p>Ganzzahl-Formatierung eines chronologischen Elements. </p>
*
* @param <V> generic type of element values (Integer, Long or Enum)
* @author Meno Hochschild
* @since 3.0
*/
class NumberProcessor<V>
implements FormatProcessor<V> {
//~ Instanzvariablen --------------------------------------------------
private final ChronoElement<V> element;
private final boolean fixedWidth;
private final int minDigits;
private final int maxDigits;
private final SignPolicy signPolicy;
private final boolean protectedMode;
private final boolean yearOfEra;
// quick path optimization
private final Leniency lenientMode;
private final int reserved;
private final char zeroDigit;
private final NumberSystem numberSystem;
private final int protectedLength;
private final int scaleOfNumsys;
// high-speed optimization
private final boolean fixedInt;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Konstruiert eine neue Instanz. </p>
*
* @param element element to be formatted
* @param fixedWidth fixed-width-mode
* @param minDigits minimum count of digits
* @param maxDigits maximum count of digits
* @param signPolicy sign policy
* @param protectedMode allow replacement?
* @throws IllegalArgumentException in case of inconsistencies
*/
NumberProcessor(
ChronoElement<V> element,
boolean fixedWidth,
int minDigits,
int maxDigits,
SignPolicy signPolicy,
boolean protectedMode
) {
this(
element, fixedWidth, minDigits, maxDigits, signPolicy, protectedMode,
0, '0', NumberSystem.ARABIC, Leniency.SMART, 0, false);
}
private NumberProcessor(
ChronoElement<V> element,
boolean fixedWidth,
int minDigits,
int maxDigits,
SignPolicy signPolicy,
boolean protectedMode,
int reserved,
char zeroDigit,
NumberSystem numberSystem,
Leniency lenientMode,
int protectedLength,
boolean fixedInt
) {
super();
this.element = element;
this.fixedWidth = fixedWidth;
this.minDigits = minDigits;
this.maxDigits = maxDigits;
this.signPolicy = signPolicy;
this.protectedMode = protectedMode;
this.fixedInt = fixedInt;
if (element == null) {
throw new NullPointerException("Missing element.");
} else if (signPolicy == null) {
throw new NullPointerException("Missing sign policy.");
} else if (minDigits < 1) {
throw new IllegalArgumentException(
"Not positive: " + minDigits);
} else if (minDigits > maxDigits) {
throw new IllegalArgumentException(
"Max smaller than min: " + maxDigits + " < " + minDigits);
} else if (fixedWidth && (minDigits != maxDigits)) {
throw new IllegalArgumentException(
"Variable width in fixed-width-mode: "
+ maxDigits + " != " + minDigits);
} else if (fixedWidth && (signPolicy != SignPolicy.SHOW_NEVER)) {
throw new IllegalArgumentException(
"Sign policy must be SHOW_NEVER in fixed-width-mode.");
}
int scale = this.getScale(NumberSystem.ARABIC);
if (minDigits > scale) {
throw new IllegalArgumentException(
"Min digits out of range: " + minDigits);
} else if (maxDigits > scale) {
throw new IllegalArgumentException(
"Max digits out of range: " + maxDigits);
}
this.yearOfEra = (this.element.name().equals("YEAR_OF_ERA"));
// quick path members
this.reserved = reserved;
this.zeroDigit = zeroDigit;
this.numberSystem = numberSystem;
this.lenientMode = lenientMode;
this.protectedLength = protectedLength;
this.scaleOfNumsys = scale;
}
//~ Methoden ----------------------------------------------------------
@SuppressWarnings("unchecked")
@Override
public void print(
ChronoDisplay formattable,
Appendable buffer,
AttributeQuery attributes,
Set<ElementPosition> positions, // optional
boolean quickPath
) throws IOException {
int start = ((buffer instanceof CharSequence) ? ((CharSequence) buffer).length() : -1);
int printed = 0;
NumberSystem numsys;
char zeroChar;
if (quickPath) {
numsys = this.numberSystem;
zeroChar = this.zeroDigit;
} else {
numsys = attributes.get(Attributes.NUMBER_SYSTEM, NumberSystem.ARABIC);
zeroChar = (
attributes.contains(Attributes.ZERO_DIGIT)
? attributes.get(Attributes.ZERO_DIGIT).charValue()
: (numsys.isDecimal() ? numsys.getDigits().charAt(0) : '0'));
}
if (quickPath && this.fixedInt) {
int v = formattable.getInt((ChronoElement<Integer>) this.element);
if (v < 0) {
if (v == Integer.MIN_VALUE) {
throw new IllegalArgumentException(
"Format context \"" + formattable + "\" without element: " + this.element);
} else {
throw new IllegalArgumentException(
"Negative value not allowed according to sign policy.");
}
}
String digits = Integer.toString(v);
int count = digits.length();
if (count > this.maxDigits) {
throw new IllegalArgumentException(
"Element " + this.element.name()
+ " cannot be printed as the formatted value " + digits
+ " exceeds the maximum width of " + this.maxDigits + ".");
}
for (int i = 0, n = this.minDigits - count; i < n; i++) {
buffer.append('0');
printed++;
}
buffer.append(digits);
printed += count;
} else if (this.yearOfEra && (this.element instanceof HistorizedElement)) {
HistorizedElement te = HistorizedElement.class.cast(this.element);
StringBuilder sb = new StringBuilder();
te.print(formattable, sb, attributes, numsys, zeroChar, this.minDigits, this.maxDigits);
buffer.append(sb.toString());
printed = sb.length();
} else {
char defaultZeroChar = numsys.getDigits().charAt(0);
Class<V> type = this.element.getType();
boolean negative = false;
String digits;
if (type == Integer.class) {
int v = formattable.getInt((ChronoElement<Integer>) this.element);
if (v == Integer.MIN_VALUE) {
throw new IllegalArgumentException(
"Format context \"" + formattable + "\" without element: " + this.element);
}
negative = (v < 0);
digits = numsys.toNumeral(Math.abs(v));
} else if (type == Long.class) {
V value = formattable.get(this.element);
long v = Long.class.cast(value).longValue();
negative = (v < 0);
digits = (
(v == Long.MIN_VALUE)
? "9223372036854775808"
: Long.toString(Math.abs(v))
);
defaultZeroChar = '0';
} else if (Enum.class.isAssignableFrom(type)) {
V value = formattable.get(this.element);
int v = -1;
if (this.element instanceof NumericalElement) {
v = ((NumericalElement<V>) this.element).numerical(value);
negative = (v < 0);
} else {
for (Object e : type.getEnumConstants()) {
if (e.equals(value)) {
v = Enum.class.cast(e).ordinal();
break;
}
}
if (v == -1) {
throw new AssertionError(
"Enum broken: " + value + " / " + type.getName());
}
}
if (v == Integer.MIN_VALUE) {
throw new IllegalArgumentException("Cannot print: " + this.element);
}
digits = numsys.toNumeral(Math.abs(v));
} else {
throw new IllegalArgumentException("Not formattable: " + this.element);
}
if (numsys.isDecimal()) {
if (zeroChar != defaultZeroChar) {
int diff = zeroChar - defaultZeroChar;
char[] characters = digits.toCharArray();
for (int i = 0; i < characters.length; i++) {
characters[i] = (char) (characters[i] + diff);
}
digits = new String(characters);
}
if (digits.length() > this.maxDigits) {
throw new IllegalArgumentException(
"Element " + this.element.name()
+ " cannot be printed as the formatted value " + digits
+ " exceeds the maximum width of " + this.maxDigits + ".");
}
}
if (negative) {
if (this.signPolicy == SignPolicy.SHOW_NEVER) {
throw new IllegalArgumentException(
"Negative value not allowed according to sign policy.");
} else {
buffer.append('-');
printed++;
}
} else {
switch (this.signPolicy) {
case SHOW_ALWAYS:
buffer.append('+');
printed++;
break;
case SHOW_WHEN_BIG_NUMBER:
if (digits.length() > this.minDigits) {
buffer.append('+');
printed++;
}
break;
default:
// no-op
}
}
if (numsys.isDecimal()) {
for (int i = 0, n = this.minDigits - digits.length(); i < n; i++) {
buffer.append(zeroChar);
printed++;
}
}
buffer.append(digits);
printed += digits.length();
}
if (
(start != -1)
&& (printed > 0)
&& (positions != null)
) {
positions.add(new ElementPosition(this.element, start, start + printed));
}
}
@Override
public void parse(
CharSequence text,
ParseLog status,
AttributeQuery attributes,
ParsedEntity<?> parsedResult,
boolean quickPath
) {
int len = text.length();
int start = status.getPosition();
if (quickPath && this.fixedInt) {
if (start >= len) {
status.setError(start, "Missing digits for: " + this.element.name());
status.setWarning();
return;
}
char sign = text.charAt(start);
if ((sign == '-') || (sign == '+')) {
status.setError(
start,
"Sign not allowed due to sign policy.");
return;
}
int minPos = start + this.minDigits;
int maxPos = Math.min(len, minPos); // maxDigits == minDigits
int total = 0;
int pos = start;
while (pos < maxPos) {
int digit = text.charAt(pos) - '0';
if ((digit >= 0) && (digit <= 9)) {
total = total * 10 + digit;
pos++;
} else {
break;
}
}
if (pos < minPos) {
if (pos == start) {
status.setError(start, "Digit expected.");
} else {
status.setError(
start,
"Not enough digits found for: " + this.element.name());
}
return;
}
parsedResult.put(this.element, total);
status.setPosition(pos);
return;
}
int protectedChars = (quickPath ? this.protectedLength : attributes.get(Attributes.PROTECTED_CHARACTERS, 0));
if (protectedChars > 0) {
len -= protectedChars;
}
if (start >= len) {
status.setError(start, "Missing digits for: " + this.element.name());
status.setWarning();
return;
}
if (this.yearOfEra && (this.element instanceof HistorizedElement)) {
HistorizedElement te = HistorizedElement.class.cast(this.element);
Object value = te.parse(text, status.getPP(), attributes, parsedResult);
if (status.isError()) {
status.setError(status.getErrorIndex(), "Unparseable element: " + this.element.name());
} else if (value == null) {
status.setError(start, "No interpretable value.");
} else {
parsedResult.put(this.element, value);
}
return;
}
NumberSystem numsys;
char zeroChar;
int effectiveMin = 1;
int effectiveMax;
if (quickPath) {
numsys = this.numberSystem;
effectiveMax = this.scaleOfNumsys;
zeroChar = this.zeroDigit;
} else {
numsys = attributes.get(Attributes.NUMBER_SYSTEM, NumberSystem.ARABIC);
effectiveMax = this.getScale(numsys);
zeroChar = (
attributes.contains(Attributes.ZERO_DIGIT)
? attributes.get(Attributes.ZERO_DIGIT).charValue()
: (numsys.isDecimal() ? numsys.getDigits().charAt(0) : '0'));
}
Leniency leniency = (quickPath ? this.lenientMode : attributes.get(Attributes.LENIENCY, Leniency.SMART));
if (this.fixedWidth || !leniency.isLax()) {
effectiveMin = this.minDigits;
effectiveMax = this.maxDigits;
}
int pos = start;
boolean negative = false;
char sign = text.charAt(pos);
if ((sign == '-') || (sign == '+')) {
if (
(this.signPolicy == SignPolicy.SHOW_NEVER)
&& (this.fixedWidth || leniency.isStrict())
) {
status.setError(
start,
"Sign not allowed due to sign policy.");
return;
} else if (
(this.signPolicy == SignPolicy.SHOW_WHEN_NEGATIVE)
&& (sign == '+')
&& leniency.isStrict()
) {
status.setError(
start,
"Positive sign not allowed due to sign policy.");
return;
}
negative = (sign == '-');
pos++;
start++;
} else if (
(this.signPolicy == SignPolicy.SHOW_ALWAYS)
&& leniency.isStrict()
) {
status.setError(start, "Missing sign of number.");
return;
}
if (pos >= len) {
status.setError(
start,
"Missing digits for: " + this.element.name());
return;
}
if (
!this.fixedWidth
&& (this.reserved > 0)
&& (protectedChars <= 0)
) {
int digitCount = 0;
// Wieviele Ziffern hat der ganze Ziffernblock?
if (numsys.isDecimal()) {
for (int i = pos; i < len; i++) {
int digit = text.charAt(i) - zeroChar;
if ((digit >= 0) && (digit <= 9)) {
digitCount++;
} else {
break;
}
}
} else {
for (int i = pos; i < len; i++) {
if (numsys.contains(text.charAt(i))) {
digitCount++;
} else {
break;
}
}
}
effectiveMax = Math.min(effectiveMax, digitCount - this.reserved);
}
int minPos = pos + effectiveMin;
int maxPos = Math.min(len, pos + effectiveMax);
long total = 0;
if (numsys.isDecimal()) {
while (pos < maxPos) {
int digit = text.charAt(pos) - zeroChar;
if ((digit >= 0) && (digit <= 9)) {
total = total * 10 + digit;
pos++;
} else {
break;
}
}
} else {
int digitCount = 0;
while (pos < maxPos) {
if (numsys.contains(text.charAt(pos))) {
digitCount++;
pos++;
} else {
break;
}
}
try {
if (digitCount > 0) {
total = numsys.toInteger(text.subSequence(pos - digitCount, pos).toString(), leniency);
}
} catch (NumberFormatException nfe) {
status.setError(start, nfe.getMessage());
return;
}
}
if (pos < minPos) {
if (pos == start) {
status.setError(start, "Digit expected.");
return;
} else if (this.fixedWidth || !leniency.isLax()) {
status.setError(
start,
"Not enough digits found for: " + this.element.name());
return;
}
}
if (negative) {
if ((total == 0) && leniency.isStrict()) {
status.setError(start - 1, "Negative zero is not allowed.");
return;
}
total = -total;
} else if (
(this.signPolicy == SignPolicy.SHOW_WHEN_BIG_NUMBER)
&& leniency.isStrict()
&& numsys.isDecimal()
) {
if ((sign == '+') && (pos <= minPos)) {
status.setError(
start - 1,
"Positive sign only allowed for big number.");
} else if ((sign != '+') && (pos > minPos)) {
status.setError(
start,
"Positive sign must be present for big number.");
}
}
Object value = null;
Class<V> type = this.element.getType();
if (type == Integer.class) {
parsedResult.put(this.element, (int) total);
status.setPosition(pos);
return;
} else if (type == Long.class) {
value = Long.valueOf(total);
} else if (this.element == PlainDate.MONTH_OF_YEAR) {
parsedResult.put(PlainDate.MONTH_AS_NUMBER, (int) total);
status.setPosition(pos);
return;
} else if (Enum.class.isAssignableFrom(type)) {
if (this.element instanceof NumericalElement) { // Normalfall
NumericalElement<V> ne = (NumericalElement<V>) this.element;
for (Object e : type.getEnumConstants()) {
if (ne.numerical(type.cast(e)) == total) {
value = e;
break;
}
}
} else {
for (Object e : type.getEnumConstants()) { // Ausweichoption
if (Enum.class.cast(e).ordinal() == total) {
value = e;
break;
}
}
}
if (value == null) {
status.setError(
((sign == '-') || (sign == '+') ? start - 1 : start),
"["
+ this.element.name()
+ "] No enum found for value: "
+ total);
return;
}
} else {
throw new IllegalArgumentException(
"Not parseable: " + this.element);
}
parsedResult.put(this.element, value);
status.setPosition(pos);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof NumberProcessor) {
NumberProcessor<?> that = (NumberProcessor<?>) obj;
return (
this.element.equals(that.element)
&& (this.fixedWidth == that.fixedWidth)
&& (this.minDigits == that.minDigits)
&& (this.maxDigits == that.maxDigits)
&& (this.signPolicy == that.signPolicy)
&& (this.protectedMode == that.protectedMode)
);
} else {
return false;
}
}
@Override
public int hashCode() {
return (
7 * this.element.hashCode()
+ 31 * (this.minDigits + this.maxDigits * 10)
);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(64);
sb.append(this.getClass().getName());
sb.append("[element=");
sb.append(this.element.name());
sb.append(", fixed-width-mode=");
sb.append(this.fixedWidth);
sb.append(", min-digits=");
sb.append(this.minDigits);
sb.append(", max-digits=");
sb.append(this.maxDigits);
sb.append(", sign-policy=");
sb.append(this.signPolicy);
sb.append(", protected-mode=");
sb.append(this.protectedMode);
sb.append(']');
return sb.toString();
}
@Override
public ChronoElement<V> getElement() {
return this.element;
}
@Override
public FormatProcessor<V> withElement(ChronoElement<V> element) {
if (
this.protectedMode
|| (this.element == element)
) {
return this;
}
return new NumberProcessor<>(
element,
this.fixedWidth,
this.minDigits,
this.maxDigits,
this.signPolicy,
false
);
}
@Override
public boolean isNumerical() {
return true;
}
@Override
public FormatProcessor<V> quickPath(
ChronoFormatter<?> formatter,
AttributeQuery attributes,
int reserved
) {
NumberSystem numsys = attributes.get(Attributes.NUMBER_SYSTEM, NumberSystem.ARABIC);
char zeroChar =
attributes.contains(Attributes.ZERO_DIGIT)
? attributes.get(Attributes.ZERO_DIGIT).charValue()
: (numsys.isDecimal() ? numsys.getDigits().charAt(0) : '0');
int plen = attributes.get(Attributes.PROTECTED_CHARACTERS, 0);
boolean hasFixedInt = (
(numsys == NumberSystem.ARABIC)
&& (zeroChar == '0')
&& this.fixedWidth
&& (plen == 0)
&& (this.element.getType() == Integer.class)
&& !this.yearOfEra
);
return new NumberProcessor<>(
this.element,
this.fixedWidth,
this.minDigits,
this.maxDigits,
this.signPolicy,
this.protectedMode,
reserved,
zeroChar,
numsys,
attributes.get(Attributes.LENIENCY, Leniency.SMART),
plen,
hasFixedInt
);
}
private int getScale(NumberSystem numsys) {
if (numsys.isDecimal()) {
return ((this.element.getType() == Long.class) ? 18 : 9);
} else {
return Integer.MAX_VALUE;
}
}
}