OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

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();
    }
}