/* * ----------------------------------------------------------------------- * Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/> * ----------------------------------------------------------------------- * This file (LiteralProcessor.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.AttributeKey; 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 java.io.IOException; import java.util.Locale; import java.util.Set; /** * <p>Formatiert ein Literal. </p> * * @author Meno Hochschild * @since 3.0 */ final class LiteralProcessor implements FormatProcessor<Void> { //~ Instanzvariablen -------------------------------------------------- private final char single; private final char alt; private final String multi; private final AttributeKey<Character> attribute; // quick path optimization private final boolean caseInsensitive; private final boolean interpunctuationMode; private final boolean rtl; //~ Konstruktoren ----------------------------------------------------- /** * <p>Konstruktor für eine feste Literalzeichenfolge. </p> * * @param literal literal char sequence * @throws IllegalArgumentException in case of inconsistencies */ LiteralProcessor(String literal) { super(); if (literal.isEmpty()) { throw new IllegalArgumentException("Missing literal."); } this.single = literal.charAt(0); this.alt = this.single; this.attribute = null; this.multi = literal; if (this.single < ' ') { throw new IllegalArgumentException( "Literal must not start with non-printable char."); } this.caseInsensitive = true; this.interpunctuationMode = ((literal.length() == 1) && isInterpunctuation(this.single)); this.rtl = false; } /** * <p>Konstruktor für ein einzelnes Zeichen mit Alternative. </p> * * @param literal preferred literal char * @param alt alternative literal char for parsing * @throws IllegalArgumentException in case of inconsistencies * @since 3.1 */ LiteralProcessor( char literal, char alt ) { super(); this.single = literal; this.alt = alt; this.attribute = null; this.multi = null; if ((literal < ' ') || (alt < ' ')) { throw new IllegalArgumentException( "Literal must not start with non-printable char."); } else if (Character.isDigit(literal) || Character.isDigit(alt)) { throw new IllegalArgumentException( "Literal must not be a decimal digit."); } this.caseInsensitive = true; this.interpunctuationMode = false; this.rtl = false; } /** * <p>Konstruktor für ein Literalzeichen, das in einem Attribut * enthalten ist. </p> * * @param attribute attribute key */ LiteralProcessor(AttributeKey<Character> attribute) { super(); if (attribute == null) { throw new NullPointerException("Missing format attribute."); } this.single = '\u0000'; this.alt = this.single; this.attribute = attribute; this.multi = null; this.caseInsensitive = true; this.interpunctuationMode = false; this.rtl = false; } private LiteralProcessor( char single, char alt, String multi, AttributeKey<Character> attribute, boolean caseInsensitive, boolean interpunctuationMode, boolean rtl ) { super(); this.single = single; this.alt = alt; this.multi = multi; this.attribute = attribute; this.caseInsensitive = caseInsensitive; this.interpunctuationMode = interpunctuationMode; this.rtl = rtl; } //~ Methoden ---------------------------------------------------------- @Override public void print( ChronoDisplay formattable, Appendable buffer, AttributeQuery attributes, Set<ElementPosition> positions, // optional boolean quickPath ) throws IOException { if (this.attribute != null) { char literal = attributes.get(this.attribute, null).charValue(); buffer.append(literal); } else if (this.multi == null) { buffer.append(this.single); } else { buffer.append(this.multi); } } @Override public void parse( CharSequence text, ParseLog status, AttributeQuery attributes, ParsedEntity<?> parsedResult, boolean quickPath ) { if (quickPath && this.interpunctuationMode) { // not relevant for RTL-languages (see quickPath()-method) int offset = status.getPosition(); if ((offset < text.length()) && (text.charAt(offset) == this.single)) { status.setPosition(offset + 1); } else { this.logError(text, status); } } else if (this.multi == null) { this.parseChar(text, status, attributes, quickPath); } else { this.parseMulti(text, status, attributes, quickPath); } } private void parseChar( CharSequence text, ParseLog status, AttributeQuery attributes, boolean quickPath ) { int offset = status.getPosition(); boolean error; char c = '\u0000'; char literal = this.single; if (this.attribute != null) { literal = attributes.get(this.attribute, Character.valueOf('\u0000')).charValue(); } if ((offset >= text.length()) || (literal == '\u0000') || Character.isDigit(literal)) { error = true; } else { c = text.charAt(offset); char alternative = this.alt; if ( (this.attribute != null) && Attributes.DECIMAL_SEPARATOR.name().equals(this.attribute.name()) && Locale.ROOT.equals(attributes.get(Attributes.LANGUAGE, Locale.ROOT)) ) { // Spezialfall: ISO-8601 alternative = ( (literal == ',') ? '.' : ((literal == '.') ? ',' : literal) ); } error = ((c != literal) && (c != alternative)); if (error) { boolean caseInsensitive = ( quickPath ? this.caseInsensitive : attributes.get(Attributes.PARSE_CASE_INSENSITIVE, Boolean.TRUE).booleanValue()); if (caseInsensitive && (charEqualsIgnoreCase(c, literal) || charEqualsIgnoreCase(c, alternative))) { error = false; } } } if (error) { StringBuilder msg = new StringBuilder("Cannot parse: \""); msg.append(text); msg.append("\" (expected: ["); msg.append(literal); msg.append("], found: ["); if (c != '\u0000') { msg.append(c); } msg.append("])"); status.setError(offset, msg.toString()); } else { status.setPosition(offset + 1); } } private void parseMulti( CharSequence text, ParseLog status, AttributeQuery attributes, boolean quickPath ) { int offset = status.getPosition(); boolean caseInsensitive = ( quickPath ? this.caseInsensitive : attributes.get(Attributes.PARSE_CASE_INSENSITIVE, Boolean.TRUE).booleanValue()); boolean rtl = ( quickPath ? this.rtl : CalendarText.isRTL(attributes.get(Attributes.LANGUAGE, Locale.ROOT))); int parsedLen = subSequenceEquals(text, offset, this.multi, caseInsensitive, rtl); if (parsedLen == -1) { this.logError(text, status); } else { status.setPosition(offset + parsedLen); } } @Override public boolean equals(Object obj) { if (this == obj) { return true; } else if (obj instanceof LiteralProcessor) { LiteralProcessor that = (LiteralProcessor) obj; if (this.attribute != null) { return this.attribute.equals(that.attribute); } else if (this.multi == null) { return ( (that.multi == null) && (this.single == that.single) && (this.alt == that.alt) ); } else { return this.multi.equals(that.multi) && (this.interpunctuationMode == that.interpunctuationMode); } } else { return false; } } @Override public int hashCode() { String ref = ( (this.attribute == null) ? ((this.multi == null) ? "" : this.multi) : this.attribute.name()); return (this.single ^ ref.hashCode()); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(this.getClass().getName()); sb.append("[literal="); if (this.attribute != null) { sb.append('{'); sb.append(this.attribute); sb.append('}'); } else if (this.multi == null) { sb.append(this.single); if (this.alt != this.single) { sb.append(", alternative="); sb.append(this.alt); } } else { sb.append(this.multi); } sb.append(']'); return sb.toString(); } // optional @Override public ChronoElement<Void> getElement() { return null; } @Override public FormatProcessor<Void> withElement(ChronoElement<Void> element) { return this; } @Override public boolean isNumerical() { if (this.multi == null) { return false; } return (this.getPrefixedDigitArea() == this.multi.length()); } @Override public FormatProcessor<Void> quickPath( ChronoFormatter<?> formatter, AttributeQuery attributes, int reserved ) { boolean rtl = CalendarText.isRTL(attributes.get(Attributes.LANGUAGE, Locale.ROOT)); return new LiteralProcessor( this.single, this.alt, this.multi, this.attribute, attributes.get(Attributes.PARSE_CASE_INSENSITIVE, Boolean.TRUE).booleanValue(), this.interpunctuationMode && !rtl, rtl ); } // count of leading digits int getPrefixedDigitArea() { if (this.multi == null) { return 0; } int digits = 0; for (int i = 0, n = this.multi.length(); i < n && Character.isDigit(this.multi.charAt(i)); i++) { digits++; } return digits; } // also used by LocalizedGMTProcessor static int subSequenceEquals( CharSequence test, int offset, CharSequence expected, boolean caseInsensitive, boolean rtl ) { int j = 0; int max = test.length(); int len = expected.length(); for (int i = 0; i < len; i++) { char c = '\u0000'; char exp = expected.charAt(i); if (isBidi(exp)) { continue; // always ignore bidis in pattern when parsing } if (rtl) { while ((j + offset < max) && isBidi(c = test.charAt(j + offset))) { j++; } } else if (j + offset < max) { c = test.charAt(j + offset); } if (j + offset >= max) { return -1; } else { j++; } if (caseInsensitive) { if (!charEqualsIgnoreCase(c, exp)) { return -1; } } else if (c != exp) { return -1; } } if (rtl) { while ((j + offset < max) && isBidi(test.charAt(j + offset))) { j++; } } return j; } 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 boolean isBidi(char c) { return ((c == '\u200E') || (c == '\u200F') || (c == '\u061C')); // LRM, RLM, ALM } private static boolean isInterpunctuation(char c) { return (!Character.isLetter(c) && !Character.isDigit(c) && !isBidi(c)); } private void logError( CharSequence text, ParseLog status ) { int offset = status.getPosition(); StringBuilder msg = new StringBuilder("Cannot parse: \""); msg.append(text); msg.append("\" (expected: ["); msg.append(this.multi); msg.append("], found: ["); msg.append(text.subSequence(offset, Math.min(offset + this.multi.length(), text.length()))); msg.append("])"); status.setError(offset, msg.toString()); } }