/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (OrdinalProcessor.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.format.Attributes;
import net.time4j.format.Leniency;
import net.time4j.format.NumberType;
import net.time4j.format.PluralCategory;
import net.time4j.format.PluralRules;
import java.io.IOException;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* <p>Ordinalzahl-Formatierung eines chronologischen Elements. </p>
*
* @author Meno Hochschild
* @since 3.0
*/
final class OrdinalProcessor
implements FormatProcessor<Integer> {
//~ Statische Felder/Initialisierungen --------------------------------
private static final Map<PluralCategory, String> ENGLISH_ORDINALS;
static {
Map<PluralCategory, String> map = new EnumMap<>(PluralCategory.class);
map.put(PluralCategory.ONE, "st");
map.put(PluralCategory.TWO, "nd");
map.put(PluralCategory.FEW, "rd");
map.put(PluralCategory.OTHER, "th");
ENGLISH_ORDINALS = Collections.unmodifiableMap(map);
}
//~ Instanzvariablen --------------------------------------------------
private final ChronoElement<Integer> element;
private final Map<PluralCategory, String> indicators; // null = english
// quick path optimization
private final int reserved;
private final int protectedLength;
private final char zeroDigit;
private final Leniency lenientMode;
private final Locale locale;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Konstruiert eine neue Instanz. </p>
*
* @param element element to be formatted
* @param indicators ordinal indicators to be used as suffixes
* @throws IllegalArgumentException in case of inconsistencies
*/
OrdinalProcessor(
ChronoElement<Integer> element,
Map<PluralCategory, String> indicators
) {
super();
if (element == null) {
throw new NullPointerException("Missing element.");
}
this.element = element;
if (indicators == null) {
this.indicators = null;
} else {
this.indicators = Collections.unmodifiableMap(new EnumMap<>(indicators));
if (!this.indicators.containsKey(PluralCategory.OTHER)) {
throw new IllegalArgumentException(
"Missing plural category OTHER: " + indicators);
}
}
this.reserved = 0;
this.protectedLength = 0;
this.zeroDigit = '0';
this.lenientMode = Leniency.SMART;
this.locale = Locale.ROOT;
}
private OrdinalProcessor(
ChronoElement<Integer> element,
Map<PluralCategory, String> indicators,
int reserved,
int protectedLength,
char zeroDigit,
Leniency lenientMode,
Locale locale
) {
super();
this.element = element;
this.indicators = indicators;
// quick path members
this.reserved = reserved;
this.protectedLength = protectedLength;
this.zeroDigit = zeroDigit;
this.lenientMode = lenientMode;
this.locale = locale;
}
//~ Methoden ----------------------------------------------------------
@Override
public void print(
ChronoDisplay formattable,
Appendable buffer,
AttributeQuery attributes,
Set<ElementPosition> positions, // optional
boolean quickPath
) throws IOException {
int value = formattable.getInt(this.element);
if (value < 0) {
if (value == Integer.MIN_VALUE) {
throw new IllegalArgumentException(
"Format context \"" + formattable + "\" without element: " + this.element);
} else {
throw new IllegalArgumentException(
"Cannot format negative ordinal numbers: " + formattable);
}
}
String digits = Integer.toString(value);
char zeroChar = (
quickPath
? this.zeroDigit
: attributes.get(Attributes.ZERO_DIGIT, Character.valueOf('0')).charValue());
if (zeroChar != '0') {
int diff = zeroChar - '0';
char[] characters = digits.toCharArray();
for (int i = 0; i < characters.length; i++) {
characters[i] = (char) (characters[i] + diff);
}
digits = new String(characters);
}
int start = -1;
int printed = 0;
if (buffer instanceof CharSequence) {
start = ((CharSequence) buffer).length();
}
buffer.append(digits);
printed += digits.length();
String indicator = this.getIndicator(attributes, quickPath, value);
buffer.append(indicator);
printed += indicator.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 effectiveMin = 1;
int effectiveMax = 9;
int len = text.length();
int start = status.getPosition();
int pos = start;
Leniency leniency = (quickPath ? this.lenientMode : attributes.get(Attributes.LENIENCY, Leniency.SMART));
int protectedChars = (quickPath ? this.protectedLength : attributes.get(Attributes.PROTECTED_CHARACTERS, 0));
if (protectedChars > 0) {
len -= protectedChars;
}
if (pos >= len) {
status.setError(pos, "Missing digits for: " + this.element.name());
status.setWarning();
return;
}
char zeroChar = (
quickPath
? this.zeroDigit
: attributes.get(Attributes.ZERO_DIGIT, Character.valueOf('0')).charValue());
if ((this.reserved > 0) && (protectedChars <= 0)) {
int digitCount = 0;
// Wieviele Ziffern hat der ganze Ziffernblock?
for (int i = pos; i < len; i++) {
int digit = text.charAt(i) - zeroChar;
if ((digit >= 0) && (digit <= 9)) {
digitCount++;
} else {
break;
}
}
effectiveMax = Math.min(effectiveMax, digitCount - this.reserved);
}
int minPos = pos + effectiveMin;
int maxPos = Math.min(len, pos + effectiveMax);
long total = 0;
boolean first = true;
while (pos < maxPos) {
int digit = text.charAt(pos) - zeroChar;
if ((digit >= 0) && (digit <= 9)) {
total = total * 10 + digit;
pos++;
first = false;
} else if (first) {
status.setError(start, "Digit expected.");
return;
} else {
break;
}
}
if (pos < minPos) {
status.setError(
start,
"Not enough digits found for: " + this.element.name());
return;
}
int value = (int) total; // safe (see effectiveMax)
String indicator = this.getIndicator(attributes, quickPath, value);
int endPos = pos + indicator.length();
if (endPos >= len) {
status.setError(
pos,
"Missing or wrong ordinal indicator for: "
+ this.element.name());
return;
}
String test = text.subSequence(pos, endPos).toString();
if (test.equals(indicator)) {
pos = endPos;
} else if (!leniency.isLax()) {
status.setError(
pos,
"Wrong ordinal indicator for: "
+ this.element.name()
+ " (expected=" + indicator + ", found=" + test + ")");
return;
}
parsedResult.put(this.element, value);
status.setPosition(pos);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof OrdinalProcessor) {
OrdinalProcessor that = (OrdinalProcessor) obj;
return (
this.element.equals(that.element)
&& this.getIndicators().equals(that.getIndicators())
);
} else {
return false;
}
}
@Override
public int hashCode() {
return (
7 * this.element.hashCode()
+ 31 * this.getIndicators().hashCode()
);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(64);
sb.append(this.getClass().getName());
sb.append("[element=");
sb.append(this.element.name());
sb.append(", indicators=");
sb.append(this.getIndicators());
sb.append(']');
return sb.toString();
}
private String getIndicator(
AttributeQuery attributes,
boolean quickPath,
int value
) {
Locale lang;
if (this.isEnglish()) {
lang = Locale.ENGLISH;
} else {
lang = (quickPath ? this.locale : attributes.get(Attributes.LANGUAGE, Locale.ROOT));
}
PluralCategory category =
PluralRules.of(lang, NumberType.ORDINALS).getCategory(value);
if (!this.getIndicators().containsKey(category)) {
category = PluralCategory.OTHER;
}
return this.getIndicators().get(category);
}
private boolean isEnglish() {
return (this.indicators == null);
}
private Map<PluralCategory, String> getIndicators() {
if (this.isEnglish()) {
return ENGLISH_ORDINALS;
}
return this.indicators;
}
@Override
public ChronoElement<Integer> getElement() {
return this.element;
}
@Override
public FormatProcessor<Integer> withElement(ChronoElement<Integer> e) {
if (this.element == e) {
return this;
}
return new OrdinalProcessor(e, this.indicators);
}
@Override
public boolean isNumerical() {
// there is also a string suffix!
return false;
}
@Override
public FormatProcessor<Integer> quickPath(
ChronoFormatter<?> formatter,
AttributeQuery attributes,
int reserved
) {
return new OrdinalProcessor(
this.element,
this.indicators,
reserved,
attributes.get(Attributes.PROTECTED_CHARACTERS, Integer.valueOf(0)).intValue(),
attributes.get(Attributes.ZERO_DIGIT, Character.valueOf('0')).charValue(),
attributes.get(Attributes.LENIENCY, Leniency.SMART),
attributes.get(Attributes.LANGUAGE, Locale.ROOT)
);
}
}