/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (TimezoneOffsetProcessor.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.base.UnixTime;
import net.time4j.engine.AttributeQuery;
import net.time4j.engine.ChronoDisplay;
import net.time4j.engine.ChronoElement;
import net.time4j.format.Attributes;
import net.time4j.format.DisplayMode;
import net.time4j.format.Leniency;
import net.time4j.tz.OffsetSign;
import net.time4j.tz.TZID;
import net.time4j.tz.Timezone;
import net.time4j.tz.ZonalOffset;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static net.time4j.format.DisplayMode.FULL;
import static net.time4j.format.DisplayMode.LONG;
import static net.time4j.format.DisplayMode.MEDIUM;
import static net.time4j.format.DisplayMode.SHORT;
import static net.time4j.tz.OffsetSign.AHEAD_OF_UTC;
import static net.time4j.tz.OffsetSign.BEHIND_UTC;
/**
* <p>Verarbeitet einen festen Zeitzonen-Offset. </p>
*
* @author Meno Hochschild
* @since 3.0
*/
final class TimezoneOffsetProcessor
implements FormatProcessor<TZID> {
//~ Statische Felder/Initialisierungen --------------------------------
/**
* <p>Spezial-Instanz nur zum Parsen im LONG-extended-Format ohne
* Ersatztext (Formatieren nicht möglich). </p>
*/
static final TimezoneOffsetProcessor EXTENDED_LONG_PARSER =
new TimezoneOffsetProcessor();
//~ Instanzvariablen --------------------------------------------------
private final DisplayMode precision;
private final boolean extended;
private final List<String> zeroOffsets;
// quick path optimization
private final boolean caseInsensitive;
private final Leniency lenientMode;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Erzeugt eine neue Instanz. </p>
*
* @param precision display mode of offset format
* @param extended extended or basic ISO-8601-mode
* @param zeroOffsets list of replacement texts if offset is zero
* @throws IllegalArgumentException if replacement text is white-space-only
*/
TimezoneOffsetProcessor(
DisplayMode precision,
boolean extended,
List<String> zeroOffsets
) {
super();
if (precision == null) {
throw new NullPointerException("Missing display mode.");
} else if (zeroOffsets.isEmpty()) {
throw new IllegalArgumentException("Missing zero offsets.");
}
List<String> offsets = new ArrayList<>(zeroOffsets);
for (String zo : offsets) {
if (zo.trim().isEmpty()) {
throw new IllegalArgumentException(
"Zero offset must not be white-space-only.");
}
}
this.precision = precision;
this.extended = extended;
this.zeroOffsets = Collections.unmodifiableList(offsets);
this.caseInsensitive = true;
this.lenientMode = Leniency.SMART;
}
private TimezoneOffsetProcessor() {
super();
this.precision = LONG;
this.extended = true;
this.zeroOffsets = Collections.emptyList();
this.caseInsensitive = true;
this.lenientMode = Leniency.SMART;
}
private TimezoneOffsetProcessor(
DisplayMode precision,
boolean extended,
List<String> zeroOffsets,
boolean caseInsensitive,
Leniency lenientMode
) {
super();
this.precision = precision;
this.extended = extended;
this.zeroOffsets = zeroOffsets;
this.caseInsensitive = caseInsensitive;
this.lenientMode = lenientMode;
}
//~ Methoden ----------------------------------------------------------
@Override
public void print(
ChronoDisplay formattable,
Appendable buffer,
AttributeQuery attributes,
Set<ElementPosition> positions,
boolean quickPath
) throws IOException {
int start = -1;
int printed = 0;
if (buffer instanceof CharSequence) {
start = ((CharSequence) buffer).length();
}
TZID tzid = null;
ZonalOffset offset;
if (formattable.hasTimezone()) {
tzid = formattable.getTimezone();
}
if (tzid == null) {
offset = getOffset(formattable, attributes);
} else if (tzid instanceof ZonalOffset) {
offset = (ZonalOffset) tzid;
} else if (formattable instanceof UnixTime) {
offset = Timezone.of(tzid).getOffset((UnixTime) formattable);
} else {
throw new IllegalArgumentException(
"Cannot extract timezone offset from: " + formattable);
}
int total = offset.getIntegralAmount();
int fraction = offset.getFractionalAmount();
if ((total | fraction) == 0) {
String zeroOffset = this.zeroOffsets.get(0);
buffer.append(zeroOffset);
printed = zeroOffset.length();
} else {
boolean negative = ((total < 0) || (fraction < 0));
buffer.append(negative ? '-' : '+');
printed++;
int absValue = Math.abs(total);
int h = absValue / 3600;
int m = (absValue / 60) % 60;
int s = absValue % 60;
if (h < 10) {
buffer.append('0');
printed++;
}
String hours = String.valueOf(h);
buffer.append(hours);
printed += hours.length();
if (
(this.precision != SHORT)
|| (m != 0)
) {
if (this.extended) {
buffer.append(':');
printed++;
}
if (m < 10) {
buffer.append('0');
printed++;
}
String minutes = String.valueOf(m);
buffer.append(minutes);
printed += minutes.length();
if (
(this.precision != SHORT)
&& (this.precision != MEDIUM)
) {
if ((this.precision == FULL) || ((s | fraction) != 0)) {
if (this.extended) {
buffer.append(':');
printed++;
}
if (s < 10) {
buffer.append('0');
printed++;
}
String seconds = String.valueOf(s);
buffer.append(seconds);
printed += seconds.length();
if (fraction != 0) {
buffer.append('.');
printed++;
String f = String.valueOf(Math.abs(fraction));
for (int i = 0, n = 9 - f.length(); i < n; i++) {
buffer.append('0');
printed++;
}
buffer.append(f);
printed += f.length();
}
}
}
}
}
if (
(start != -1)
&& (printed > 0)
&& (positions != null)
) {
positions.add(
new ElementPosition(
TimezoneElement.TIMEZONE_ID,
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();
int pos = start;
if (pos >= len) {
status.setError(start, "Missing timezone offset.");
return;
}
for (String zeroOffset : this.zeroOffsets) {
int zl = zeroOffset.length();
if (len - pos >= zl) {
String compare = text.subSequence(pos, pos + zl).toString();
boolean ignoreCase = (
quickPath
? this.caseInsensitive
: attributes.get(Attributes.PARSE_CASE_INSENSITIVE, Boolean.TRUE).booleanValue());
if (
(ignoreCase && compare.equalsIgnoreCase(zeroOffset))
|| (!ignoreCase && compare.equals(zeroOffset))
) {
parsedResult.put(TimezoneElement.TIMEZONE_OFFSET, ZonalOffset.UTC);
status.setPosition(pos + zl);
return;
}
}
}
Leniency leniency = (quickPath ? this.lenientMode : attributes.get(Attributes.LENIENCY, Leniency.SMART));
char c = text.charAt(pos);
OffsetSign sign;
if (c == '+') {
sign = AHEAD_OF_UTC;
pos++;
} else if (c == '-') {
sign = BEHIND_UTC;
pos++;
} else if (Character.isDigit(c) && leniency.isLax()) {
sign = AHEAD_OF_UTC;
} else {
status.setError(start, "Missing sign of timezone offset.");
return;
}
int hours = parseNum(text, pos, leniency);
if (hours == -1000) {
status.setError(
pos,
"Hour part in timezone offset "
+ "does not match expected pattern HH.");
return;
}
if (hours < 0) {
hours = ~hours;
pos++;
} else {
pos += 2;
}
if (pos >= len) {
if (this.precision == SHORT) {
parsedResult.put(
TimezoneElement.TIMEZONE_OFFSET,
ZonalOffset.ofHours(sign, hours));
status.setPosition(pos);
} else {
status.setError(
pos,
"Missing minute part in timezone offset.");
}
return;
}
int colon = 0;
if (this.extended) {
if (text.charAt(pos) == ':') {
colon = 1;
} else if (this.precision == SHORT) {
parsedResult.put(
TimezoneElement.TIMEZONE_OFFSET,
ZonalOffset.ofHours(sign, hours));
status.setPosition(pos);
return;
} else {
status.setError(pos, "Colon expected in timezone offset.");
return;
}
}
int minutes = parseNum(text, pos + colon, Leniency.STRICT);
int seconds = 0;
int fraction = 0;
if (minutes == -1000) {
if (this.precision == SHORT) {
parsedResult.put(
TimezoneElement.TIMEZONE_OFFSET,
ZonalOffset.ofHours(sign, hours));
status.setPosition(pos);
} else {
status.setError(
pos + colon,
"Minute part in timezone offset "
+ "does not match expected pattern mm.");
}
return;
}
pos += colon;
pos += 2;
if (
(pos < len)
&& ((this.precision == LONG) || (this.precision == FULL))
) {
colon = 0;
if (this.extended) {
if (text.charAt(pos) == ':') {
colon = 1;
seconds = parseNum(text, pos + colon, Leniency.STRICT);
} else if (this.precision == FULL) {
status.setError(pos, "Colon expected in timezone offset.");
return;
} else {
seconds = -1000;
}
} else {
seconds = parseNum(text, pos, Leniency.STRICT);
}
if (seconds == -1000) {
if (this.precision == FULL) {
status.setError(
pos,
"Second part in timezone offset "
+ "does not match expected pattern ss.");
return;
} else {
seconds = 0;
}
} else {
pos += colon;
pos += 2;
if (pos + 10 <= len) {
char dot = text.charAt(pos);
if (dot == '.') {
pos++;
for (int i = pos, n = pos + 9; i < n; i++) {
char digit = text.charAt(i);
if ((digit >= '0') && (digit <= '9')) {
fraction = fraction * 10 + (digit - '0');
pos++;
} else {
status.setError(
pos,
"9 digits in fractional part of "
+ "timezone offset expected.");
return;
}
}
}
}
}
}
ZonalOffset offset;
if (
(seconds == 0)
&& (fraction == 0)
) {
offset = ZonalOffset.ofHoursMinutes(sign, hours, minutes);
} else {
int total = hours * 3600 + minutes * 60 + seconds;
if (sign == BEHIND_UTC) {
total = -total;
fraction = -fraction;
}
offset = ZonalOffset.ofTotalSeconds(total, fraction);
}
parsedResult.put(TimezoneElement.TIMEZONE_OFFSET, offset);
status.setPosition(pos);
}
@Override
public ChronoElement<TZID> getElement() {
return TimezoneElement.TIMEZONE_OFFSET;
}
@Override
public FormatProcessor<TZID> withElement(ChronoElement<TZID> element) {
return this;
}
@Override
public boolean isNumerical() {
return false;
}
@Override
public FormatProcessor<TZID> quickPath(
ChronoFormatter<?> formatter,
AttributeQuery attributes,
int reserved
) {
return new TimezoneOffsetProcessor(
this.precision,
this.extended,
this.zeroOffsets,
attributes.get(Attributes.PARSE_CASE_INSENSITIVE, Boolean.TRUE).booleanValue(),
attributes.get(Attributes.LENIENCY, Leniency.SMART)
);
}
private static ZonalOffset getOffset(
ChronoDisplay formattable,
AttributeQuery attributes
) {
if (attributes.contains(Attributes.TIMEZONE_ID)) {
TZID tzid = attributes.get(Attributes.TIMEZONE_ID);
if (tzid instanceof ZonalOffset) {
return (ZonalOffset) tzid;
} else if (tzid != null) {
throw new IllegalArgumentException(
"Use a timezone offset instead of ["
+ tzid.canonical()
+ "] when formatting ["
+ formattable
+ "].");
}
}
throw new IllegalArgumentException(
"Cannot extract timezone offset from format attributes for: "
+ formattable);
}
private static int parseNum(
CharSequence text,
int pos,
Leniency leniency
) {
int total = 0;
for (int i = 0; i < 2; i++) {
char c;
if (pos + i >= text.length()) {
c = '\u0000';
} else {
c = text.charAt(pos + i);
}
if ((c >= '0') && (c <= '9')) {
total = total * 10 + (c - '0');
} else if ((i == 0) || leniency.isStrict()) {
return -1000;
} else {
return ~total;
}
}
return total;
}
}