/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (HistoricalIntegerElement.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.history;
import net.time4j.Month;
import net.time4j.PlainDate;
import net.time4j.base.GregorianDate;
import net.time4j.base.MathUtils;
import net.time4j.engine.AttributeQuery;
import net.time4j.engine.BasicElement;
import net.time4j.engine.CalendarDays;
import net.time4j.engine.ChronoDisplay;
import net.time4j.engine.ChronoElement;
import net.time4j.engine.ChronoEntity;
import net.time4j.engine.ChronoException;
import net.time4j.engine.Chronology;
import net.time4j.engine.ElementRule;
import net.time4j.format.Attributes;
import net.time4j.format.CalendarText;
import net.time4j.format.Leniency;
import net.time4j.format.NumberSystem;
import net.time4j.format.OutputContext;
import net.time4j.format.TextAccessor;
import net.time4j.format.TextWidth;
import net.time4j.history.internal.HistorizedElement;
import net.time4j.history.internal.StdHistoricalElement;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectStreamException;
import java.text.ParsePosition;
import java.util.List;
import java.util.Locale;
import static net.time4j.history.YearDefinition.DUAL_DATING;
/**
* <p>Allgemeines verstellbares chronologisches Element auf Integer-Basis. </p>
*
* @author Meno Hochschild
* @since 3.0
*/
final class HistoricIntegerElement
extends StdHistoricalElement
implements HistorizedElement {
//~ Statische Felder/Initialisierungen --------------------------------
static final int YEAR_OF_ERA_INDEX = 2;
static final int MONTH_INDEX = 3;
static final int DAY_OF_MONTH_INDEX = 4;
static final int DAY_OF_YEAR_INDEX = 5;
static final int YEAR_AFTER_INDEX = 6;
static final int YEAR_BEFORE_INDEX = 7;
static final int CENTURY_INDEX = 8;
private static final long serialVersionUID = -6283098762945747308L;
//~ Instanzvariablen --------------------------------------------------
/**
* @serial associated chronological history
*/
private final ChronoHistory history;
private transient final int index;
//~ Konstruktoren -----------------------------------------------------
HistoricIntegerElement(
char symbol,
int defaultMin,
int defaultMax,
ChronoHistory history,
int index
) {
super(toName(index), symbol, defaultMin, defaultMax);
this.history = history;
this.index = index;
}
//~ Methoden ----------------------------------------------------------
@Override
public void print(
ChronoDisplay context,
Appendable buffer,
AttributeQuery attributes
) throws IOException {
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'));
this.print(context, buffer, attributes, numsys, zeroChar, 1, 9);
}
@Override
public void print(
ChronoDisplay context,
Appendable buffer,
AttributeQuery attributes,
NumberSystem numsys,
char zeroChar,
int minDigits,
int maxDigits
) throws IOException {
if (this.index == DAY_OF_YEAR_INDEX) {
buffer.append(String.valueOf(context.get(this.history.dayOfYear())));
return;
}
HistoricDate date;
if (context instanceof GregorianDate) {
date = this.history.convert(PlainDate.from((GregorianDate) context));
} else {
date = context.get(this.history.date());
}
switch (this.index) {
case YEAR_OF_ERA_INDEX:
NewYearStrategy nys = this.history.getNewYearStrategy();
int yearOfEra = date.getYearOfEra();
String text = null;
if (!NewYearStrategy.DEFAULT.equals(nys)) {
int yearOfDisplay = date.getYearOfEra(nys);
if (yearOfDisplay != yearOfEra) {
if (attributes.get(ChronoHistory.YEAR_DEFINITION, DUAL_DATING) == DUAL_DATING) {
text = this.dual(numsys, zeroChar, yearOfDisplay, yearOfEra, minDigits);
} else {
yearOfEra = yearOfDisplay;
}
}
}
if (text == null) { // no dual format
if (numsys.isDecimal()) {
text = pad(numsys.toNumeral(yearOfEra), minDigits, zeroChar);
} else {
text = numsys.toNumeral(yearOfEra);
}
}
if (numsys.isDecimal()) {
char defaultZeroChar = numsys.getDigits().charAt(0);
if (zeroChar != defaultZeroChar) {
StringBuilder sb = new StringBuilder();
for (int i = 0, n = text.length(); i < n; i++) {
char c = text.charAt(i);
if (numsys.contains(c)) {
int diff = zeroChar - defaultZeroChar;
sb.append((char) (c + diff));
} else {
sb.append(c); // - (minus) or / (slash)
}
}
text = sb.toString();
}
this.checkLength(text, maxDigits);
}
buffer.append(text);
break;
case MONTH_INDEX:
OutputContext oc = attributes.get(Attributes.OUTPUT_CONTEXT, OutputContext.FORMAT);
buffer.append(this.monthAccessor(attributes, oc).print(Month.valueOf(date.getMonth())));
break;
case DAY_OF_MONTH_INDEX:
buffer.append(String.valueOf(date.getDayOfMonth()));
break;
default:
throw new ChronoException("Not printable as text: " + this.name());
}
}
@Override
public Integer parse(
CharSequence text,
ParsePosition status,
AttributeQuery attributes
) {
return this.parse(text, status, attributes, null);
}
@Override
public Integer parse(
CharSequence text,
ParsePosition status,
AttributeQuery attributes,
ChronoEntity<?> parsedResult
) {
if (this.index == MONTH_INDEX) {
int index = status.getIndex();
OutputContext oc = attributes.get(Attributes.OUTPUT_CONTEXT, OutputContext.FORMAT);
Month month = this.monthAccessor(attributes, oc).parse(text, status, Month.class, attributes);
if ((month == null) && attributes.get(Attributes.PARSE_MULTIPLE_CONTEXT, Boolean.TRUE)) {
status.setErrorIndex(-1);
status.setIndex(index);
oc = ((oc == OutputContext.FORMAT) ? OutputContext.STANDALONE : OutputContext.FORMAT);
month = this.monthAccessor(attributes, oc).parse(text, status, Month.class, attributes);
}
if (month == null) {
return null;
} else {
return Integer.valueOf(month.getValue());
}
} else if (
(this.index == YEAR_AFTER_INDEX)
|| (this.index == YEAR_BEFORE_INDEX)
|| (this.index == CENTURY_INDEX)
) {
throw new ChronoException("Not parseable as text element: " + this.name());
}
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'));
Leniency leniency = (numsys.isDecimal() ? Leniency.SMART : attributes.get(Attributes.LENIENCY, Leniency.SMART));
int start = status.getIndex();
int pos = start;
int value = parseNum(numsys, zeroChar, text, pos, status, leniency);
pos = status.getIndex();
if ( // dual date check
(this.index == YEAR_OF_ERA_INDEX)
&& (pos > start)
&& (!NewYearStrategy.DEFAULT.equals(this.history.getNewYearStrategy()))
&& (pos < text.length())
&& (text.charAt(pos) == '/')
&& (attributes.get(ChronoHistory.YEAR_DEFINITION, DUAL_DATING) == DUAL_DATING)
) {
int slash = pos;
int yoe = parseNum(numsys, zeroChar, text, pos + 1, status, leniency);
int test = status.getIndex();
if (test == pos + 1) { // we will now stop consuming more chars and ignore yoe-part
status.setIndex(pos);
} else {
pos = test;
int yod = value;
int maxDeviation = (
(this.history.getNewYearStrategy().rule(HistoricEra.AD, yod) == NewYearRule.CALCULUS_PISANUS)
? 2 : 1);
int ancient = this.getAncientYear(yod, yoe, maxDeviation);
if (numsys.isDecimal() && (ancient != Integer.MAX_VALUE)) {
value = ancient;
if (parsedResult != null) {
parsedResult.with(StdHistoricalElement.YEAR_OF_DISPLAY, yod);
}
} else if (Math.abs(yoe - yod) <= maxDeviation) { // plausibility check
value = yoe;
if (parsedResult != null) {
parsedResult.with(StdHistoricalElement.YEAR_OF_DISPLAY, yod);
}
} else { // now we have something else - let the formatter process the rest
value = yod;
pos = slash;
status.setIndex(pos);
}
}
}
if (pos == start) {
status.setErrorIndex(start);
return null;
} else {
return Integer.valueOf(value);
}
}
@Override
protected <T extends ChronoEntity<T>> ElementRule<T, Integer> derive(Chronology<T> chronology) {
if (chronology.isRegistered(PlainDate.COMPONENT)) {
return new Rule<>(this.index, this.history);
}
return null;
}
@Override
protected boolean isSingleton() {
return false;
}
@Override
protected boolean doEquals(BasicElement<?> obj) {
return this.history.equals(((HistoricIntegerElement) obj).history);
}
private int getAncientYear(
int yearOfDisplay,
int yearOfEra,
int maxDeviation
) {
if ((yearOfEra >= 0) && (yearOfEra < 100) && (yearOfDisplay >= 100)) {
int factor = ((yearOfEra < 10) ? 10 : 100);
if (Math.abs(yearOfEra - MathUtils.floorModulo(yearOfDisplay, factor)) <= maxDeviation) {
return MathUtils.floorDivide(yearOfDisplay, factor) * factor + yearOfEra;
}
}
return Integer.MAX_VALUE;
}
private String dual(
NumberSystem numsys,
char zeroChar,
int yearOfDisplay,
int yearOfEra,
int minDigits
) {
StringBuilder sb = new StringBuilder();
sb.append(numsys.toNumeral(yearOfDisplay));
sb.append('/');
if (
numsys.isDecimal()
&& (yearOfEra >= 100)
&& (MathUtils.floorDivide(yearOfDisplay, 100) == MathUtils.floorDivide(yearOfEra, 100))
) {
int yoe2 = MathUtils.floorModulo(yearOfEra, 100);
if (yoe2 < 10) {
sb.append(zeroChar);
}
sb.append(numsys.toNumeral(yoe2));
} else {
sb.append(numsys.toNumeral(yearOfEra));
}
if (numsys.isDecimal()) {
return pad(sb.toString(), minDigits, zeroChar);
} else {
return sb.toString();
}
}
private static String pad(
String digits,
int min,
char zeroChar
) {
int len = digits.length();
if (min <= len) {
return digits; // optimization
}
StringBuilder sb = new StringBuilder();
for (int i = 0, n = min - len; i < n; i++) {
sb.append(zeroChar);
}
sb.append(digits);
return sb.toString();
}
private void checkLength(
String digits,
int max
) {
int len = digits.length();
if (len > max) {
throw new IllegalArgumentException(
"Element " + this.name()
+ " cannot be printed as the formatted value " + digits
+ " exceeds the maximum width of " + max + ".");
}
}
private TextAccessor monthAccessor(
AttributeQuery attributes,
OutputContext outputContext
) {
CalendarText cnames = CalendarText.getIsoInstance(attributes.get(Attributes.LANGUAGE, Locale.ROOT));
TextWidth textWidth = attributes.get(Attributes.TEXT_WIDTH, TextWidth.WIDE);
return cnames.getStdMonths(textWidth, outputContext);
}
private static int parseNum(
NumberSystem numsys,
char zeroChar,
CharSequence text,
int offset,
ParsePosition status,
Leniency leniency
) {
int value = 0;
int pos = offset;
if (numsys.isDecimal()) {
boolean negative = false;
if ((numsys == NumberSystem.ARABIC) && (text.charAt(pos) == '-')) {
negative = true;
pos++;
}
char defaultZeroChar = (leniency.isStrict() ? '\u0000' : numsys.getDigits().charAt(0));
for (int i = pos, n = Math.min(pos + 9, text.length()); i < n; i++) {
int digit = text.charAt(i) - zeroChar;
if ((digit >= 0) && (digit <= 9)) {
value = value * 10 + digit;
pos++;
} else if ((defaultZeroChar != '\u0000') && (zeroChar != defaultZeroChar)) { // smart or lax mode
digit = text.charAt(i) - defaultZeroChar;
if ((digit >= 0) && (digit <= 9)) {
zeroChar = defaultZeroChar;
value = value * 10 + digit;
pos++;
} else {
break;
}
} else {
break;
}
}
if (negative) {
if (pos == offset + 1) {
pos = offset;
} else {
value = MathUtils.safeNegate(value);
}
}
} else {
int len = 0;
for (int i = pos, n = text.length(); i < n; i++) {
if (numsys.contains(text.charAt(i))) {
len++;
} else {
break;
}
}
if (len > 0) {
value = numsys.toInteger(text.subSequence(pos, pos + len).toString(), leniency);
pos += len;
}
}
status.setIndex(pos);
return value;
}
private static String toName(int index) {
switch (index) {
case YEAR_OF_ERA_INDEX:
return "YEAR_OF_ERA";
case MONTH_INDEX:
return "HISTORIC_MONTH";
case DAY_OF_MONTH_INDEX:
return "HISTORIC_DAY_OF_MONTH";
case DAY_OF_YEAR_INDEX:
return "HISTORIC_DAY_OF_YEAR";
case YEAR_AFTER_INDEX:
return "YEAR_AFTER";
case YEAR_BEFORE_INDEX:
return "YEAR_BEFORE";
case CENTURY_INDEX:
return "CENTURY_OF_ERA";
default:
throw new UnsupportedOperationException("Unknown element index: " + index);
}
}
private Object readResolve() throws ObjectStreamException {
String n = this.name();
switch (n) {
case "YEAR_OF_ERA":
return this.history.yearOfEra();
case "HISTORIC_MONTH":
return this.history.month();
case "HISTORIC_DAY_OF_MONTH":
return this.history.dayOfMonth();
case "HISTORIC_DAY_OF_YEAR":
return this.history.dayOfYear();
case "YEAR_AFTER":
return this.history.yearOfEra(YearDefinition.AFTER_NEW_YEAR);
case "YEAR_BEFORE":
return this.history.yearOfEra(YearDefinition.BEFORE_NEW_YEAR);
case "CENTURY_OF_ERA":
return this.history.centuryOfEra();
default:
throw new InvalidObjectException("Unknown element: " + n);
}
}
//~ Innere Klassen ----------------------------------------------------
private static class Rule<C extends ChronoEntity<C>>
implements ElementRule<C, Integer> {
//~ Instanzvariablen ----------------------------------------------
private final int index;
private final ChronoHistory history;
//~ Konstruktoren -------------------------------------------------
Rule(
int index,
ChronoHistory history
) {
super();
this.index = index;
this.history = history;
}
//~ Methoden ------------------------------------------------------
@Override
public Integer getValue(C context) {
try {
PlainDate iso = context.get(PlainDate.COMPONENT);
HistoricDate date = this.history.convert(iso);
int value;
switch (this.index) {
case YEAR_OF_ERA_INDEX:
value = date.getYearOfEra();
break;
case MONTH_INDEX:
value = date.getMonth();
break;
case DAY_OF_MONTH_INDEX:
value = date.getDayOfMonth();
break;
case DAY_OF_YEAR_INDEX:
long utc = iso.getDaysSinceEpochUTC();
int yoe = date.getYearOfEra(this.history.getNewYearStrategy());
HistoricDate newYear = this.history.getBeginOfYear(date.getEra(), yoe);
value = (int) (utc - this.history.convert(newYear).getDaysSinceEpochUTC() + 1);
break;
case YEAR_AFTER_INDEX:
case YEAR_BEFORE_INDEX:
value = date.getYearOfEra(this.history.getNewYearStrategy());
break;
case CENTURY_INDEX:
value = ((date.getYearOfEra() - 1) / 100) + 1;
break;
default:
throw new UnsupportedOperationException("Unknown element index: " + this.index);
}
return Integer.valueOf(value);
} catch (IllegalArgumentException iae) {
throw new ChronoException(iae.getMessage(), iae);
}
}
@Override
public Integer getMinimum(C context) {
try {
HistoricDate current = this.history.convert(context.get(PlainDate.COMPONENT));
if (
(this.index == YEAR_OF_ERA_INDEX)
|| (this.index == YEAR_AFTER_INDEX)
|| (this.index == YEAR_BEFORE_INDEX)
|| (this.index == CENTURY_INDEX)
) {
if ((current.getEra() == HistoricEra.BYZANTINE) && (current.getMonth() >= 9)) {
return Integer.valueOf(0);
} else {
return Integer.valueOf(1);
}
}
HistoricDate hd = this.adjust(context, 1);
if (this.history.isValid(hd)) {
return Integer.valueOf(1);
} else if (this.index == DAY_OF_YEAR_INDEX) {
throw new ChronoException("Historic New Year cannot be determined.");
}
List<CutOverEvent> events = this.history.getEvents();
for (int i = events.size() - 1; i >= 0; i--) {
CutOverEvent event = events.get(i);
if (current.compareTo(event.dateAtCutOver) >= 0) {
hd = event.dateAtCutOver;
break;
}
}
int min = ((this.index == MONTH_INDEX) ? hd.getMonth() : hd.getDayOfMonth());
return Integer.valueOf(min);
} catch (IllegalArgumentException iae) {
throw new ChronoException(iae.getMessage(), iae);
}
}
@Override
public Integer getMaximum(C context) {
try {
HistoricDate current = this.history.convert(context.get(PlainDate.COMPONENT));
HistoricDate hd;
int max;
switch (this.index) {
case YEAR_OF_ERA_INDEX:
case YEAR_AFTER_INDEX:
case YEAR_BEFORE_INDEX:
case CENTURY_INDEX:
if (current.getEra() == HistoricEra.BC) {
max = this.history.convert(PlainDate.axis().getMinimum()).getYearOfEra();
} else {
max = this.history.convert(PlainDate.axis().getMaximum()).getYearOfEra();
}
if (this.index == CENTURY_INDEX) {
max = ((max - 1) / 100) + 1;
}
return Integer.valueOf(max);
case MONTH_INDEX:
max = 12;
hd = this.adjust(context, max);
break;
case DAY_OF_MONTH_INDEX:
max = this.history.getAlgorithm(current).getMaximumDayOfMonth(current);
hd = this.adjust(context, max);
break;
case DAY_OF_YEAR_INDEX:
int yoe = current.getYearOfEra(this.history.getNewYearStrategy());
max = this.history.getLengthOfYear(current.getEra(), yoe);
if (max == -1) {
throw new ChronoException("Length of historic year undefined.");
}
return Integer.valueOf(max);
default:
throw new UnsupportedOperationException("Unknown element index: " + this.index);
}
if (this.history.isValid(hd)) {
return Integer.valueOf(max);
}
List<CutOverEvent> events = this.history.getEvents();
CutOverEvent candidate;
for (int i = events.size() - 1; i >= 0; i--) {
CutOverEvent event = events.get(i);
candidate = event;
if (current.compareTo(event.dateAtCutOver) < 0) {
hd = candidate.dateBeforeCutOver;
break;
}
}
max = ((this.index == MONTH_INDEX) ? hd.getMonth() : hd.getDayOfMonth());
return Integer.valueOf(max);
} catch (IllegalArgumentException iae) {
throw new ChronoException(iae.getMessage(), iae);
}
}
@Override
public boolean isValid(
C context,
Integer value
) {
if (value == null) {
return false;
}
try {
HistoricDate newHD = this.adjust(context, value.intValue());
return this.history.isValid(newHD);
} catch (IllegalArgumentException iae) {
return false;
}
}
@Override
public C withValue(
C context,
Integer value,
boolean lenient
) {
if (value == null) {
throw new IllegalArgumentException("Missing historic element value.");
}
HistoricDate newHD = this.adjust(context, value.intValue());
return context.with(PlainDate.COMPONENT, this.history.convert(newHD));
}
@Override
public ChronoElement<?> getChildAtFloor(C context) {
throw new UnsupportedOperationException("Never called.");
}
@Override
public ChronoElement<?> getChildAtCeiling(C context) {
throw new UnsupportedOperationException("Never called.");
}
private HistoricDate adjust(
C context,
int value
) {
HistoricDate hd = this.history.convert(context.get(PlainDate.COMPONENT));
YearDefinition yd = YearDefinition.DUAL_DATING;
NewYearStrategy nys = this.history.getNewYearStrategy();
HistoricDate result;
switch (this.index) {
case YEAR_AFTER_INDEX:
case YEAR_BEFORE_INDEX:
yd = (
(this.index == YEAR_AFTER_INDEX)
? YearDefinition.AFTER_NEW_YEAR
: YearDefinition.BEFORE_NEW_YEAR);
// fall-through
case YEAR_OF_ERA_INDEX:
result = HistoricDate.of(hd.getEra(), value, hd.getMonth(), hd.getDayOfMonth(), yd, nys);
result = this.history.adjustDayOfMonth(result);
break;
case MONTH_INDEX:
result = HistoricDate.of(hd.getEra(), hd.getYearOfEra(), value, hd.getDayOfMonth());
result = this.history.adjustDayOfMonth(result);
break;
case DAY_OF_MONTH_INDEX:
result = HistoricDate.of(hd.getEra(), hd.getYearOfEra(), hd.getMonth(), value);
break;
case DAY_OF_YEAR_INDEX:
int yoe = hd.getYearOfEra(this.history.getNewYearStrategy());
HistoricDate newYear = this.history.getBeginOfYear(hd.getEra(), yoe);
int max = this.history.getLengthOfYear(hd.getEra(), yoe);
if (value == 1) {
result = newYear;
} else if ((value > 1) && (value <= max)) {
PlainDate date = this.history.convert(newYear);
date = date.plus(CalendarDays.of(value - 1));
result = this.history.convert(date);
} else {
throw new IllegalArgumentException("Out of range: " + value);
}
break;
case CENTURY_INDEX:
int y2 = (hd.getYearOfEra() % 100);
int yearOfEra = ((value - 1) * 100) + ((y2 == 0) ? 100 : y2);
result = HistoricDate.of(hd.getEra(), yearOfEra, hd.getMonth(), hd.getDayOfMonth(), yd, nys);
result = this.history.adjustDayOfMonth(result);
break;
default:
throw new UnsupportedOperationException("Unknown element index: " + this.index);
}
return result;
}
}
}