Dépôt officiel du code source de l'ERP OpenConcerto
Blame | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU General Public License Version 3
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*/
package org.openconcerto.utils.text;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.StringUtils;
import org.openconcerto.utils.text.DateTimeFormat.Literal;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.SignStyle;
import java.time.format.TextStyle;
import java.time.temporal.ChronoField;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.ibm.icu.text.DateTimePatternGenerator;
public final class DateProp extends DateTimeFormatComponent {
static public final DateProp YEAR = new DateProp("year with four digits");
static public final DateProp MONTH_NAME = new DateProp("full name of the month");
static public final DateProp MONTH_NUMBER = new DateProp("2 digits number of the month (starting at 1)");
static public final DateProp DAY_IN_MONTH = new DateProp("2 digits day number in the month");
static public final DateProp DAY_NAME_IN_WEEK = new DateProp("full name of day");
static public final DateProp HOUR = new DateProp("hour in day (00-23)");
static public final DateProp MINUTE = new DateProp("minute in hour");
static public final DateProp SECOND = new DateProp("second in minute");
static public final DateProp MICROSECOND = new DateProp("microseconds (000000-999999)");
static public final Set<DateProp> ALL_INSTANCES = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(YEAR, MONTH_NAME, MONTH_NUMBER, DAY_IN_MONTH, DAY_NAME_IN_WEEK, HOUR, MINUTE, SECOND, MICROSECOND)));
static public final Set<DateProp> LOCALE_SENSITIVE_INSTANCES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(MONTH_NAME, DAY_NAME_IN_WEEK)));
static public final DateTimeSkeleton TIME_SKELETON = DateTimeFormatBuilder.buildSkeleton(HOUR, MINUTE, SECOND);
static public final DateTimeSkeleton SHORT_DATE_SKELETON = DateTimeFormatBuilder.buildSkeleton(DAY_IN_MONTH, MONTH_NUMBER, YEAR);
static public final DateTimeSkeleton LONG_DATE_SKELETON = DateTimeFormatBuilder.buildSkeleton(DAY_NAME_IN_WEEK, DAY_IN_MONTH, MONTH_NAME, YEAR);
static public final DateTimeSkeleton SHORT_DATETIME_SKELETON = DateTimeFormatBuilder.buildSkeleton(DAY_IN_MONTH, MONTH_NUMBER, YEAR, HOUR, MINUTE);
static public final DateTimeSkeleton LONG_DATETIME_SKELETON = DateTimeFormatBuilder.buildSkeleton(DAY_NAME_IN_WEEK, DAY_IN_MONTH, MONTH_NAME, YEAR, HOUR, MINUTE, SECOND);
// pure format (i.e. no literal string)
static private final Map<DateProp, String> JAVA_DATE_SPECS_PURE;
static public final Map<DateProp, String> DATE_PROP_TO_JAVA;
static private final SortedMap<String, DateProp> REVERSE_JAVA_SPEC;
static private final Pattern REVERSE_SPEC_PATTERN;
static {
JAVA_DATE_SPECS_PURE = new HashMap<DateProp, String>();
JAVA_DATE_SPECS_PURE.put(YEAR, "yyyy");
JAVA_DATE_SPECS_PURE.put(MONTH_NAME, "MMMM");
JAVA_DATE_SPECS_PURE.put(MONTH_NUMBER, "MM");
JAVA_DATE_SPECS_PURE.put(DAY_IN_MONTH, "dd");
JAVA_DATE_SPECS_PURE.put(DAY_NAME_IN_WEEK, "EEEE");
JAVA_DATE_SPECS_PURE.put(HOUR, "HH");
JAVA_DATE_SPECS_PURE.put(MINUTE, "mm");
JAVA_DATE_SPECS_PURE.put(SECOND, "ss");
final Map<DateProp, String> tmp = new HashMap<>(DateProp.JAVA_DATE_SPECS_PURE);
tmp.put(DateProp.MICROSECOND, "SSS000");
DATE_PROP_TO_JAVA = Collections.unmodifiableMap(tmp);
assert DATE_PROP_TO_JAVA.keySet().equals(ALL_INSTANCES);
// reverse, so longer strings come first (e.g. MMMM|MM to match the longer one)
final SortedMap<String, DateProp> m = new TreeMap<>(Collections.reverseOrder());
REVERSE_JAVA_SPEC = CollectionUtils.invertMap(m, JAVA_DATE_SPECS_PURE);
assert REVERSE_JAVA_SPEC.size() == JAVA_DATE_SPECS_PURE.size() : "Duplicate values";
assert !JAVA_DATE_SPECS_PURE.containsKey(null) : "Null spec";
assert !JAVA_DATE_SPECS_PURE.containsValue(null) : "Null value";
REVERSE_SPEC_PATTERN = Pattern.compile(CollectionUtils.join(REVERSE_JAVA_SPEC.keySet(), "|"));
}
/**
* Convert the passed pattern to a {@link SimpleDateFormat} pattern.
*
* @param simpleFormat either {@link #ALL_INSTANCES} or arbitrary literal string that will be
* quoted.
* @return the {@link SimpleDateFormat java} pattern.
*/
public static String toJavaPattern(final List<? extends DateTimeFormatComponent> simpleFormat) {
return toStringPattern(simpleFormat, DATE_PROP_TO_JAVA::get, StringUtils::singleQuote);
}
public static final String toStringPattern(final List<? extends DateTimeFormatComponent> simpleFormat, final Function<? super DateProp, String> datePropToString,
final Function<String, String> literalToString) {
final StringBuilder sb = new StringBuilder(simpleFormat.size() * 6);
// needed because if there's 2 consecutive literal text then 'text1''text2' which evaluates
// to text1'text2.
final StringBuilder literalText = new StringBuilder(64);
for (final DateTimeFormatComponent p : simpleFormat) {
if (p instanceof Literal) {
literalText.append(((Literal) p).getValue());
} else {
final String javaComp = Objects.requireNonNull(datePropToString.apply((DateProp) p));
if (literalText.length() > 0) {
sb.append(literalToString.apply(literalText.toString()));
literalText.setLength(0);
}
sb.append(javaComp);
}
}
if (literalText.length() > 0) {
sb.append(literalToString.apply(literalText.toString()));
}
return sb.toString();
}
public static DateTimeFormatterBuilder createJavaFormatterBuilder(final List<? extends DateTimeFormatComponent> simpleFormat) {
final DateTimeFormatterBuilder res = new DateTimeFormatterBuilder();
for (final DateTimeFormatComponent p : simpleFormat) {
if (p instanceof Literal) {
res.appendLiteral(((Literal) p).getValue());
} else {
// from DateTimeFormatterBuilder.appendPattern()
if (p == YEAR)
res.appendValue(ChronoField.YEAR, 4, 19, SignStyle.EXCEEDS_PAD);
else if (p == MONTH_NAME)
res.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL);
else if (p == MONTH_NUMBER)
res.appendValue(ChronoField.MONTH_OF_YEAR, 2);
else if (p == DAY_IN_MONTH)
res.appendValue(ChronoField.DAY_OF_MONTH, 2);
else if (p == DAY_NAME_IN_WEEK)
res.appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL);
else if (p == HOUR)
res.appendValue(ChronoField.HOUR_OF_DAY, 2);
else if (p == MINUTE)
res.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
else if (p == SECOND)
res.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
else if (p == MICROSECOND)
res.appendValue(ChronoField.MICRO_OF_SECOND, 6);
else
throw new IllegalArgumentException("Unknown " + p);
}
}
return res;
}
/**
* Return the best pattern matching the input skeleton.
*
* @param simpleFormat the fields needed, e.g. [YEAR, DAY_IN_MONTH, MONTH_NUMBER].
* @param l the locale needed.
* @return the best match, e.g. "dd/MM/yyyy" for {@link Locale#FRANCE} , "MM/dd/yyyy" for
* {@link Locale#US}.
*/
public static String getBestJavaPattern(final List<DateProp> simpleFormat, final Locale l) {
final StringBuilder sb = new StringBuilder(128);
for (final DateProp p : simpleFormat) {
final String javaComp = JAVA_DATE_SPECS_PURE.get(p);
if (javaComp != null)
sb.append(javaComp);
else
throw new IllegalArgumentException("Unsupported spec : " + p);
}
// needs same length so our pattern works
return DateTimePatternGenerator.getInstance(l).getBestPattern(sb.toString(), DateTimePatternGenerator.MATCH_ALL_FIELDS_LENGTH);
}
/**
* Return the best pattern matching the input skeleton.
*
* @param simpleFormat the fields needed, e.g. [YEAR, DAY_IN_MONTH, MONTH_NUMBER].
* @param l the locale needed.
* @return the best match, e.g. [DAY_IN_MONTH, "/", MONTH_NUMBER, "/", YEAR] for
* {@link Locale#FRANCE} , [MONTH_NUMBER, "/", DAY_IN_MONTH, "/", YEAR] for
* {@link Locale#US}.
*/
public static DateTimeFormat getBestPattern(final List<DateProp> simpleFormat, final Locale l) {
return parseJavaPattern(getBestJavaPattern(simpleFormat, l));
}
static DateTimeFormat parseJavaPattern(final String bestPattern) {
final Matcher matcher = REVERSE_SPEC_PATTERN.matcher(bestPattern);
final Matcher quotedMatcher = StringUtils.SINGLE_QUOTED_PATTERN.matcher(bestPattern);
final DateTimeFormatBuilder res = new DateTimeFormatBuilder();
int index = 0;
while (index < bestPattern.length()) {
final int quoteIndex = bestPattern.indexOf('\'', index);
final int endSansQuote = quoteIndex < 0 ? bestPattern.length() : quoteIndex;
// parse quote-free string
matcher.region(index, endSansQuote);
while (matcher.find()) {
if (index < matcher.start())
res.addLiteral(bestPattern.substring(index, matcher.start()));
res.add(REVERSE_JAVA_SPEC.get(matcher.group()));
index = matcher.end();
}
assert index <= endSansQuote : "region() failed";
if (index < endSansQuote)
res.addLiteral(bestPattern.substring(index, endSansQuote));
index = endSansQuote;
// parse quoted string
if (index < bestPattern.length()) {
quotedMatcher.region(index, bestPattern.length());
if (!quotedMatcher.find() || quotedMatcher.start() != quotedMatcher.regionStart())
throw new IllegalStateException("Quoted string error : " + bestPattern.substring(quoteIndex));
res.addLiteral(StringUtils.unSingleQuote(quotedMatcher));
index = quotedMatcher.end();
}
}
return res.build();
}
private final String name;
private DateProp(String name) {
this.name = name;
}
public final String getName() {
return this.name;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + ' ' + this.getName();
}
}