/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (FractionProcessor.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.engine.AttributeQuery;
import net.time4j.engine.ChronoDisplay;
import net.time4j.engine.ChronoElement;
import net.time4j.engine.ChronoEntity;
import net.time4j.format.Attributes;
import net.time4j.format.Leniency;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Set;
/**
* <p>Fraktionale Formatierung eines Sekundenbruchteils. </p>
*
* @author Meno Hochschild
* @since 3.0
*/
final class FractionProcessor
implements FormatProcessor<Integer> {
//~ Statische Felder/Initialisierungen --------------------------------
private static final int MRD_MINUS_1 = 999_999_999;
//~ Instanzvariablen --------------------------------------------------
private final FormatProcessor<Void> decimalSeparator;
private final ChronoElement<Integer> element;
private final int minDigits;
private final int maxDigits;
private final boolean fixedWidth;
// quick path optimization
private final char zeroDigit;
private final Leniency lenientMode;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Konstruiert eine neue Instanz. </p>
*
* @param element element to be formatted in a fractional way
* @param minDigits minimum count of digits
* @param maxDigits maximum count of digits
* @param decimalSeparator shall decimal separator be visible?
*/
FractionProcessor(
ChronoElement<Integer> element,
int minDigits,
int maxDigits,
boolean decimalSeparator
) {
super();
this.element = element;
this.minDigits = minDigits;
this.maxDigits = maxDigits;
this.fixedWidth = (!decimalSeparator && (minDigits == maxDigits));
this.decimalSeparator = (
decimalSeparator
? new LiteralProcessor(Attributes.DECIMAL_SEPARATOR)
: null);
if (element == null) {
throw new NullPointerException("Missing element.");
} else if (minDigits < 0) {
throw new IllegalArgumentException(
"Negative min digits: " + minDigits);
} else if (minDigits > maxDigits) {
throw new IllegalArgumentException(
"Max smaller than min: " + maxDigits + " < " + minDigits);
}
if (minDigits > 9) {
throw new IllegalArgumentException(
"Min digits out of range: " + minDigits);
} else if (maxDigits > 9) {
throw new IllegalArgumentException(
"Max digits out of range: " + maxDigits);
}
this.zeroDigit = '0';
this.lenientMode = Leniency.SMART;
}
private FractionProcessor(
FormatProcessor<Void> decimalSeparator,
ChronoElement<Integer> element,
int minDigits,
int maxDigits,
boolean fixedWidth,
char zeroDigit,
Leniency lenientMode
) {
super();
this.decimalSeparator = decimalSeparator;
this.element = element;
this.minDigits = minDigits;
this.maxDigits = maxDigits;
this.fixedWidth = fixedWidth;
// quick path members
this.zeroDigit = zeroDigit;
this.lenientMode = lenientMode;
}
//~ Methoden ----------------------------------------------------------
@Override
public void print(
ChronoDisplay formattable,
Appendable buffer,
AttributeQuery attributes,
Set<ElementPosition> positions, // optional
boolean quickPath
) throws IOException {
BigDecimal value = toDecimal(formattable.get(this.element));
BigDecimal min = toDecimal(formattable.getMinimum(this.element));
BigDecimal max = toDecimal(formattable.getMaximum(this.element));
if (value.compareTo(max) > 0) {
value = max;
}
BigDecimal fraction =
value.subtract(min).divide(
max.subtract(min).add(BigDecimal.ONE),
9,
RoundingMode.FLOOR);
fraction = (
(fraction.compareTo(BigDecimal.ZERO) == 0)
? BigDecimal.ZERO
: fraction.stripTrailingZeros()
);
char zeroChar = (
quickPath
? this.zeroDigit
: attributes.get(Attributes.ZERO_DIGIT, Character.valueOf('0')).charValue());
int start = -1;
int printed = 0;
if (buffer instanceof CharSequence) {
start = ((CharSequence) buffer).length();
}
if (fraction.scale() == 0) {
// scale ist 0, wenn value das Minimum ist
if (this.minDigits > 0) {
if (this.hasDecimalSeparator()) {
this.decimalSeparator.print(
formattable,
buffer,
attributes,
positions,
quickPath);
printed++;
}
for (int i = 0; i < this.minDigits; i++) {
buffer.append(zeroChar);
}
printed += this.minDigits;
}
} else {
if (this.hasDecimalSeparator()) {
this.decimalSeparator.print(
formattable,
buffer,
attributes,
positions,
quickPath);
printed++;
}
int outputScale =
Math.min(
Math.max(fraction.scale(), this.minDigits),
this.maxDigits);
fraction = fraction.setScale(outputScale, RoundingMode.FLOOR);
String digits = fraction.toPlainString();
int diff = zeroChar - '0';
for (int i = 2, n = digits.length(); i < n; i++) {
char c = (char) (digits.charAt(i) + diff);
buffer.append(c);
printed++;
}
}
if (
(start != -1)
&& (printed > 1)
&& (positions != null)
) {
positions.add( // Zählung ohne Dezimaltrennzeichen
new ElementPosition(this.element, start + 1, start + printed));
}
}
@Override
public void parse(
CharSequence text,
ParseLog status,
AttributeQuery attributes,
ParsedEntity<?> parsedResult,
boolean quickPath
) {
Leniency leniency = (quickPath ? this.lenientMode : attributes.get(Attributes.LENIENCY, Leniency.SMART));
int effectiveMin = 0;
int effectiveMax = 9;
if (!leniency.isLax() || this.fixedWidth) {
effectiveMin = this.minDigits;
effectiveMax = this.maxDigits;
}
int len = text.length();
if (status.getPosition() >= len) {
if (effectiveMin > 0) {
status.setError(
status.getPosition(),
"Expected fraction digits not found for: "
+ this.element.name());
}
return;
}
if (this.hasDecimalSeparator()) {
this.decimalSeparator.parse(
text,
status,
attributes,
null,
quickPath);
if (status.isError()) {
if (effectiveMin == 0) {
status.clearError();
}
return;
}
}
int current = status.getPosition();
int minEndPos = current + effectiveMin;
int maxEndPos = Math.min(current + effectiveMax, len);
if ((minEndPos > len) && leniency.isStrict()) {
status.setError(
status.getPosition(),
"Expected at least " + effectiveMin + " digits.");
return;
}
char zeroChar = (
quickPath
? this.zeroDigit
: attributes.get(Attributes.ZERO_DIGIT, Character.valueOf('0')).charValue());
long total = 0;
while (current < maxEndPos) {
int digit = text.charAt(current) - zeroChar;
if ((digit >= 0) && (digit <= 9)) {
total = total * 10 + digit;
current++;
} else if ((current < minEndPos) && leniency.isStrict()) {
status.setError(
status.getPosition(),
"Expected at least " + effectiveMin + " digits.");
return;
} else {
break;
}
}
BigDecimal fraction = new BigDecimal(total);
fraction = fraction.movePointLeft(current - status.getPosition());
if (this.element.name().equals("NANO_OF_SECOND")) {
int num = this.getRealValue(fraction, 0, MRD_MINUS_1);
parsedResult.put(this.element, num);
} else {
// hier nur prototypischer Wert, später fraktionalen Wert bestimmen
parsedResult.put(FractionalElement.FRACTION, fraction);
parsedResult.put(this.element, this.element.getDefaultMinimum());
}
status.setPosition(current);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof FractionProcessor) {
FractionProcessor that = (FractionProcessor) obj;
return (
this.element.equals(that.element)
&& (this.minDigits == that.minDigits)
&& (this.maxDigits == that.maxDigits)
&& (this.hasDecimalSeparator() == that.hasDecimalSeparator())
);
} 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(", min-digits=");
sb.append(this.minDigits);
sb.append(", max-digits=");
sb.append(this.maxDigits);
sb.append(']');
return sb.toString();
}
@Override
public ChronoElement<Integer> getElement() {
return this.element;
}
@Override
public FormatProcessor<Integer> withElement(
ChronoElement<Integer> element
) {
if (this.element == element) {
return this;
}
return new FractionProcessor(
element,
this.minDigits,
this.maxDigits,
this.hasDecimalSeparator()
);
}
@Override
public boolean isNumerical() {
return true;
}
@Override
public FormatProcessor<Integer> quickPath(
ChronoFormatter<?> formatter,
AttributeQuery attributes,
int reserved
) {
return new FractionProcessor(
this.decimalSeparator,
this.element,
this.minDigits,
this.maxDigits,
this.fixedWidth,
attributes.get(Attributes.ZERO_DIGIT, Character.valueOf('0')).charValue(),
attributes.get(Attributes.LENIENCY, Leniency.SMART)
);
}
/**
* <p>Aktualisiert das prototypische Parse-Ergebnis mit dem richtigen
* Wert. </p>
*
* <p>In der ersten Phase wurde prototypisch nur das Standardminimum
* des Elements als Wert angenommen. In dieser Phase wird stattdessen der
* geparste {@code BigDecimal}-Wert in den neuen Elementwert
* übersetzt und damit das Ergebnis angepasst. </p>
*
* @param entity prototypical result of parsing
* @param parsed intermediate buffer for parsed values
* @return updated result object
*/
ChronoEntity<?> update(
ChronoEntity<?> entity,
ChronoEntity<?> parsed
) {
if (!parsed.contains(FractionalElement.FRACTION)) {
return entity;
}
BigDecimal fraction = parsed.get(FractionalElement.FRACTION);
int min = entity.getMinimum(this.element).intValue();
int max = entity.getMaximum(this.element).intValue();
int num = this.getRealValue(fraction, min, max);
parsed.with(FractionalElement.FRACTION, null); // mutable
parsed.with(this.element, num); // mutable
return entity.with(this.element, num);
}
private int getRealValue(
BigDecimal fraction,
int min,
int max
) {
BigDecimal low = BigDecimal.valueOf(min);
BigDecimal range =
BigDecimal.valueOf(max)
.subtract(low)
.add(BigDecimal.ONE);
BigDecimal value =
fraction.multiply(range)
.setScale(0, RoundingMode.FLOOR)
.add(low);
return value.intValueExact();
}
private static BigDecimal toDecimal(Number num) {
return BigDecimal.valueOf(num.longValue());
}
private boolean hasDecimalSeparator() {
return (this.decimalSeparator != null);
}
}