/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (LocalizedGMTProcessor.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.CalendarText;
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.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static net.time4j.tz.OffsetSign.AHEAD_OF_UTC;
import static net.time4j.tz.OffsetSign.BEHIND_UTC;
/**
* <p>Verarbeitet einen lokalisierten Zeitzonen-Offset. </p>
*
* @author Meno Hochschild
* @since 3.0
*/
final class LocalizedGMTProcessor
implements FormatProcessor<TZID> {
//~ Statische Felder/Initialisierungen --------------------------------
// private static final char UNICODE_LRM = '\u200E';
private static final ZonalOffset PROTOTYPE = ZonalOffset.ofTotalSeconds(3600 * 18);
private static final ConcurrentMap<Locale, String> UTC_LITERALS = new ConcurrentHashMap<>();
private static final ConcurrentMap<Locale, Info> STD_PATTERN_INFOS = new ConcurrentHashMap<>();
//~ Instanzvariablen --------------------------------------------------
private final boolean abbreviated;
// quick path optimization
private final boolean caseInsensitive;
private final boolean noPrefix;
private final Locale locale;
private final String plusSign;
private final String minusSign;
private final char zeroDigit;
private final Leniency lenientMode;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Erzeugt eine neue Instanz. </p>
*
* @param abbreviated short form of localized gmt offset?
*/
LocalizedGMTProcessor(boolean abbreviated) {
this(abbreviated, true, false, Locale.ROOT, "+", "-", '0', Leniency.SMART);
}
private LocalizedGMTProcessor(
boolean abbreviated,
boolean caseInsensitive,
boolean noPrefix,
Locale locale,
String plusSign,
String minusSign,
char zeroDigit,
Leniency lenientMode
) {
super();
this.abbreviated = abbreviated;
// quick path members
this.caseInsensitive = caseInsensitive;
this.noPrefix = noPrefix;
this.locale = locale;
this.plusSign = plusSign;
this.minusSign = minusSign;
this.zeroDigit = zeroDigit;
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);
}
Locale loc = (quickPath ? this.locale : attributes.get(Attributes.LANGUAGE, Locale.ROOT));
char zeroChar = (
quickPath
? this.zeroDigit
: attributes.get(Attributes.ZERO_DIGIT, Character.valueOf('0')).charValue());
String plus = (quickPath ? this.plusSign : attributes.get(AttributeSet.PLUS_SIGN, "+"));
String minus = (quickPath ? this.minusSign : attributes.get(AttributeSet.MINUS_SIGN, "-"));
// hack for cldr-version before v30
// if ("ar".equals(loc.getLanguage()) && (zeroChar == '0')) {
// plus = UNICODE_LRM + "+";
// minus = UNICODE_LRM + "\u002D";
// }
boolean np = (
quickPath
? this.noPrefix
: attributes.get(Attributes.NO_GMT_PREFIX, Boolean.FALSE).booleanValue());
int total = offset.getIntegralAmount();
int fraction = offset.getFractionalAmount();
if (!np && (total == 0) && (fraction == 0)) {
String literal = getLiteralUTC(loc);
buffer.append(literal);
printed = literal.length();
} else {
Info info = getPatternInfo(loc);
for (int p = 0, n = info.pattern.length(); p < n; p++) {
char c = info.pattern.charAt(p);
// literal
if ((info.start > p) || (info.end <= p)) {
if (!np) {
buffer.append(c);
printed++;
}
continue;
}
// offset sign
if (offset.getSign() == BEHIND_UTC) {
buffer.append(minus);
printed += minus.length();
} else {
buffer.append(plus);
printed += plus.length();
}
// hour part
int h = offset.getAbsoluteHours();
int m = offset.getAbsoluteMinutes();
int s = offset.getAbsoluteSeconds();
if ((h < 10) && !this.abbreviated) {
buffer.append(zeroChar);
printed++;
}
String hours = String.valueOf(h);
for (int i = 0; i < hours.length(); i++) {
char digit = (char) (hours.charAt(i) - '0' + zeroChar);
buffer.append(digit);
printed++;
}
// minute part
if ((m != 0) || (s != 0) || !this.abbreviated) {
buffer.append(info.separator);
printed += info.separator.length();
if (m < 10) {
buffer.append(zeroChar);
printed++;
}
String minutes = String.valueOf(m);
for (int i = 0; i < minutes.length(); i++) {
char digit = (char) (minutes.charAt(i) - '0' + zeroChar);
buffer.append(digit);
printed++;
}
// second part
if (s != 0) {
buffer.append(info.separator);
printed += info.separator.length();
if (s < 10) {
buffer.append(zeroChar);
printed++;
}
String seconds = String.valueOf(s);
for (int i = 0; i < seconds.length(); i++) {
char digit = (char) (seconds.charAt(i) - '0' + zeroChar);
buffer.append(digit);
printed++;
}
}
}
p = (info.end - 1);
}
}
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 localized time zone offset.");
return;
}
Locale loc = (quickPath ? this.locale : attributes.get(Attributes.LANGUAGE, Locale.ROOT));
boolean rtl = CalendarText.isRTL(loc);
boolean np = (
quickPath
? this.noPrefix
: attributes.get(Attributes.NO_GMT_PREFIX, Boolean.FALSE).booleanValue());
boolean ignoreCase = (
quickPath
? this.caseInsensitive
: attributes.get(Attributes.PARSE_CASE_INSENSITIVE, Boolean.TRUE).booleanValue());
char zeroChar = (
quickPath
? this.zeroDigit
: attributes.get(Attributes.ZERO_DIGIT, Character.valueOf('0')).charValue());
String plus = (quickPath ? this.plusSign : attributes.get(AttributeSet.PLUS_SIGN, "+"));
String minus = (quickPath ? this.minusSign : attributes.get(AttributeSet.MINUS_SIGN, "-"));
// hack for cldr-version before v30
// if ("ar".equals(loc.getLanguage()) && (zeroChar == '0')) {
// plus = UNICODE_LRM + "+";
// minus = UNICODE_LRM + "\u002D";
// }
Info info = getPatternInfo(loc);
int n = info.pattern.length();
ZonalOffset offset = null;
int old = pos;
for (int p = 0; p < n; p++) {
char c = info.pattern.charAt(p);
// literal
if ((info.start > p) || (info.end <= p)) {
if (!np) {
char test = (pos < len) ? text.charAt(pos) : '\u0000';
if (
(!ignoreCase && (c == test))
|| (ignoreCase && charEqualsIgnoreCase(c, test))
) {
pos++;
} else {
int zl = parseUTC(text, len, old, loc, ignoreCase); // try other literal
if (zl > 0) {
parsedResult.put(TimezoneElement.TIMEZONE_OFFSET, ZonalOffset.UTC);
status.setPosition(old + zl);
} else {
status.setError(
start,
"Literal mismatched in localized time zone offset.");
}
return;
}
}
continue;
}
OffsetSign sign;
int parsedLen = LiteralProcessor.subSequenceEquals(text, pos, plus, ignoreCase, rtl);
if (parsedLen == -1) {
parsedLen = LiteralProcessor.subSequenceEquals(text, pos, minus, ignoreCase, rtl);
if (parsedLen == -1) {
int zl = (np ? 0 : parseUTC(text, len, old, loc, ignoreCase)); // no sign => try UTC
if (zl > 0) {
parsedResult.put(TimezoneElement.TIMEZONE_OFFSET, ZonalOffset.UTC);
status.setPosition(old + zl);
return;
} else {
status.setError(
start,
"Missing sign in localized time zone offset.");
return;
}
} else {
sign = BEHIND_UTC;
}
} else {
sign = AHEAD_OF_UTC;
}
pos += parsedLen;
int hours = parseHours(text, pos, zeroChar);
if (hours == -1000) {
status.setError(
pos,
"Missing hour part in localized time zone offset.");
return;
}
if (hours < 0) {
hours = ~hours;
pos++;
} else {
pos += 2;
}
if (pos >= len) {
if (this.abbreviated) {
parsedResult.put(
TimezoneElement.TIMEZONE_OFFSET,
ZonalOffset.ofHours(sign, hours));
status.setPosition(pos);
} else {
status.setError(
pos,
"Missing minute part in localized time zone offset.");
}
return;
}
Leniency leniency = (quickPath ? this.lenientMode : attributes.get(Attributes.LENIENCY, Leniency.SMART));
int seplen = LiteralProcessor.subSequenceEquals(text, pos, info.separator, ignoreCase, rtl);
if (seplen != -1) {
pos += seplen;
} else if (this.abbreviated) {
parsedResult.put(
TimezoneElement.TIMEZONE_OFFSET,
ZonalOffset.ofHours(sign, hours));
status.setPosition(pos);
return;
} else if (leniency.isStrict()) {
status.setError(pos, "Mismatch of localized time zone offset separator.");
return;
}
int minutes = parseTwoDigits(text, pos, zeroChar);
if (minutes == -1000) {
status.setError(
pos,
"Minute part in localized time zone offset does not match expected pattern mm.");
return;
}
pos += 2;
int seconds = 0;
if (pos < len) {
seplen = LiteralProcessor.subSequenceEquals(text, pos, info.separator, ignoreCase, rtl);
if (seplen != -1) {
pos += seplen;
seconds = parseTwoDigits(text, pos, zeroChar);
if (seconds == -1000) {
pos -= seplen;
} else {
pos += 2;
}
}
}
if ((seconds == 0) || (seconds == -1000)) {
offset = ZonalOffset.ofHoursMinutes(sign, hours, minutes);
} else {
int total = hours * 3600 + minutes * 60 + seconds;
if (sign == OffsetSign.BEHIND_UTC) {
total = -total;
}
offset = ZonalOffset.ofTotalSeconds(total);
}
p = (info.end - 1);
}
if (offset == null) {
status.setError(
pos,
"Unable to determine localized time zone offset.");
} else {
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 LocalizedGMTProcessor(
this.abbreviated,
attributes.get(Attributes.PARSE_CASE_INSENSITIVE, Boolean.TRUE).booleanValue(),
attributes.get(Attributes.NO_GMT_PREFIX, Boolean.FALSE).booleanValue(),
attributes.get(Attributes.LANGUAGE, Locale.ROOT),
attributes.get(AttributeSet.PLUS_SIGN, "+"),
attributes.get(AttributeSet.MINUS_SIGN, "-"),
attributes.get(Attributes.ZERO_DIGIT, Character.valueOf('0')).charValue(),
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;
}
}
throw new IllegalArgumentException(
"Cannot extract timezone offset from format attributes for: "
+ formattable);
}
private static int parseUTC(
CharSequence text,
int len,
int pos,
Locale loc,
boolean ignoreCase
) {
String gmtPrefix = getLiteralUTC(loc);
String[] zeroOffsets = {"GMT", gmtPrefix, "UTC", "UT"};
for (String zeroOffset : zeroOffsets) {
int test = zeroOffset.length();
if (len - pos >= test) {
String compare = text.subSequence(pos, pos + test).toString();
if (
(ignoreCase && compare.equalsIgnoreCase(zeroOffset))
|| (!ignoreCase && compare.equals(zeroOffset))
) {
return test;
}
}
}
return 0;
}
private static int parseTwoDigits(
CharSequence text,
int pos,
char zeroDigit
) {
int total = 0;
for (int i = 0; i < 2; i++) {
int digit;
if (pos + i >= text.length()) {
return -1000;
} else {
digit = text.charAt(pos + i) - zeroDigit;
}
if ((digit >= 0) && (digit <= 9)) {
total = total * 10 + digit;
} else {
return -1000;
}
}
return total;
}
private static int parseHours(
CharSequence text,
int pos,
char zeroDigit
) {
int total = 0;
for (int i = 0; i < 2; i++) {
int digit;
if (pos + i >= text.length()) {
if (i == 0) {
return -1000;
} else {
return ~total;
}
} else {
digit = text.charAt(pos + i) - zeroDigit;
}
if ((digit >= 0) && (digit <= 9)) {
total = total * 10 + digit;
} else if (i == 0) {
return -1000;
} else {
return ~total;
}
}
return total;
}
private static boolean charEqualsIgnoreCase(
char c1,
char c2
) {
return (
(c1 == c2)
|| (Character.toUpperCase(c1) == Character.toUpperCase(c2))
|| (Character.toLowerCase(c1) == Character.toLowerCase(c2))
);
}
private static String getLiteralUTC(Locale locale) {
String pattern = UTC_LITERALS.get(locale);
if (pattern == null) {
pattern = ZonalOffset.UTC.getStdFormatPattern(locale);
String old = UTC_LITERALS.putIfAbsent(locale, pattern);
if (old != null) {
pattern = old;
}
}
return pattern;
}
private static Info getPatternInfo(Locale locale) {
Info info = STD_PATTERN_INFOS.get(locale);
if (info == null){
String offsetPattern = PROTOTYPE.getStdFormatPattern(locale);
for (int i = 0, n = offsetPattern.length(); i < n; i++) {
if (offsetPattern.charAt(i) == '\u00B1') {
int sep1 = offsetPattern.indexOf("hh", i) + 2;
int sep2 = offsetPattern.indexOf("mm", sep1);
info = new Info(offsetPattern, offsetPattern.substring(sep1, sep2), i, sep2 + 2);
Info old = STD_PATTERN_INFOS.putIfAbsent(locale, info);
if (old != null) {
info = old;
}
break;
}
}
}
assert (info != null);
return info;
}
//~ Innere Klassen ----------------------------------------------------
private static class Info {
//~ Instanzvariablen ----------------------------------------------
private final String pattern;
private final String separator;
private final int start;
private final int end;
//~ Konstruktoren -------------------------------------------------
Info(
String pattern,
String separator,
int start,
int end
) {
super();
this.pattern = pattern;
this.separator = separator;
this.start = start;
this.end = end;
}
}
}