OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
182 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.utils.text;
15
 
16
import org.openconcerto.utils.CollectionUtils;
17
import org.openconcerto.utils.StringUtils;
18
import org.openconcerto.utils.text.DateTimeFormat.Literal;
19
 
20
import java.text.SimpleDateFormat;
21
import java.time.format.DateTimeFormatterBuilder;
22
import java.time.format.SignStyle;
23
import java.time.format.TextStyle;
24
import java.time.temporal.ChronoField;
25
import java.util.Arrays;
26
import java.util.Collections;
27
import java.util.HashMap;
28
import java.util.HashSet;
29
import java.util.List;
30
import java.util.Locale;
31
import java.util.Map;
32
import java.util.Objects;
33
import java.util.Set;
34
import java.util.SortedMap;
35
import java.util.TreeMap;
36
import java.util.function.Function;
37
import java.util.regex.Matcher;
38
import java.util.regex.Pattern;
39
 
40
import com.ibm.icu.text.DateTimePatternGenerator;
41
 
42
public final class DateProp extends DateTimeFormatComponent {
43
    static public final DateProp YEAR = new DateProp("year with four digits");
44
    static public final DateProp MONTH_NAME = new DateProp("full name of the month");
45
    static public final DateProp MONTH_NUMBER = new DateProp("2 digits number of the month (starting at 1)");
46
    static public final DateProp DAY_IN_MONTH = new DateProp("2 digits day number in the month");
47
    static public final DateProp DAY_NAME_IN_WEEK = new DateProp("full name of day");
48
    static public final DateProp HOUR = new DateProp("hour in day (00-23)");
49
    static public final DateProp MINUTE = new DateProp("minute in hour");
50
    static public final DateProp SECOND = new DateProp("second in minute");
51
    static public final DateProp MICROSECOND = new DateProp("microseconds (000000-999999)");
52
 
53
    static public final Set<DateProp> ALL_INSTANCES = Collections
54
            .unmodifiableSet(new HashSet<>(Arrays.asList(YEAR, MONTH_NAME, MONTH_NUMBER, DAY_IN_MONTH, DAY_NAME_IN_WEEK, HOUR, MINUTE, SECOND, MICROSECOND)));
55
    static public final Set<DateProp> LOCALE_SENSITIVE_INSTANCES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(MONTH_NAME, DAY_NAME_IN_WEEK)));
56
 
57
    static public final DateTimeSkeleton TIME_SKELETON = DateTimeFormatBuilder.buildSkeleton(HOUR, MINUTE, SECOND);
58
    static public final DateTimeSkeleton SHORT_DATE_SKELETON = DateTimeFormatBuilder.buildSkeleton(DAY_IN_MONTH, MONTH_NUMBER, YEAR);
59
    static public final DateTimeSkeleton LONG_DATE_SKELETON = DateTimeFormatBuilder.buildSkeleton(DAY_NAME_IN_WEEK, DAY_IN_MONTH, MONTH_NAME, YEAR);
60
    static public final DateTimeSkeleton SHORT_DATETIME_SKELETON = DateTimeFormatBuilder.buildSkeleton(DAY_IN_MONTH, MONTH_NUMBER, YEAR, HOUR, MINUTE);
61
    static public final DateTimeSkeleton LONG_DATETIME_SKELETON = DateTimeFormatBuilder.buildSkeleton(DAY_NAME_IN_WEEK, DAY_IN_MONTH, MONTH_NAME, YEAR, HOUR, MINUTE, SECOND);
62
 
63
    // pure format (i.e. no literal string)
64
    static private final Map<DateProp, String> JAVA_DATE_SPECS_PURE;
65
    static public final Map<DateProp, String> DATE_PROP_TO_JAVA;
66
    static private final SortedMap<String, DateProp> REVERSE_JAVA_SPEC;
67
    static private final Pattern REVERSE_SPEC_PATTERN;
68
 
69
    static {
70
        JAVA_DATE_SPECS_PURE = new HashMap<DateProp, String>();
71
        JAVA_DATE_SPECS_PURE.put(YEAR, "yyyy");
72
        JAVA_DATE_SPECS_PURE.put(MONTH_NAME, "MMMM");
73
        JAVA_DATE_SPECS_PURE.put(MONTH_NUMBER, "MM");
74
        JAVA_DATE_SPECS_PURE.put(DAY_IN_MONTH, "dd");
75
        JAVA_DATE_SPECS_PURE.put(DAY_NAME_IN_WEEK, "EEEE");
76
        JAVA_DATE_SPECS_PURE.put(HOUR, "HH");
77
        JAVA_DATE_SPECS_PURE.put(MINUTE, "mm");
78
        JAVA_DATE_SPECS_PURE.put(SECOND, "ss");
79
 
80
        final Map<DateProp, String> tmp = new HashMap<>(DateProp.JAVA_DATE_SPECS_PURE);
81
        tmp.put(DateProp.MICROSECOND, "SSS000");
82
        DATE_PROP_TO_JAVA = Collections.unmodifiableMap(tmp);
83
        assert DATE_PROP_TO_JAVA.keySet().equals(ALL_INSTANCES);
84
 
85
        // reverse, so longer strings come first (e.g. MMMM|MM to match the longer one)
86
        final SortedMap<String, DateProp> m = new TreeMap<>(Collections.reverseOrder());
87
        REVERSE_JAVA_SPEC = CollectionUtils.invertMap(m, JAVA_DATE_SPECS_PURE);
88
        assert REVERSE_JAVA_SPEC.size() == JAVA_DATE_SPECS_PURE.size() : "Duplicate values";
89
        assert !JAVA_DATE_SPECS_PURE.containsKey(null) : "Null spec";
90
        assert !JAVA_DATE_SPECS_PURE.containsValue(null) : "Null value";
91
 
92
        REVERSE_SPEC_PATTERN = Pattern.compile(CollectionUtils.join(REVERSE_JAVA_SPEC.keySet(), "|"));
93
    }
94
 
95
    /**
96
     * Convert the passed pattern to a {@link SimpleDateFormat} pattern.
97
     *
98
     * @param simpleFormat either {@link #ALL_INSTANCES} or arbitrary literal string that will be
99
     *        quoted.
100
     * @return the {@link SimpleDateFormat java} pattern.
101
     */
102
    public static String toJavaPattern(final List<? extends DateTimeFormatComponent> simpleFormat) {
103
        return toStringPattern(simpleFormat, DATE_PROP_TO_JAVA::get, StringUtils::singleQuote);
104
    }
105
 
106
    public static final String toStringPattern(final List<? extends DateTimeFormatComponent> simpleFormat, final Function<? super DateProp, String> datePropToString,
107
            final Function<String, String> literalToString) {
108
        final StringBuilder sb = new StringBuilder(simpleFormat.size() * 6);
109
        // needed because if there's 2 consecutive literal text then 'text1''text2' which evaluates
110
        // to text1'text2.
111
        final StringBuilder literalText = new StringBuilder(64);
112
        for (final DateTimeFormatComponent p : simpleFormat) {
113
            if (p instanceof Literal) {
114
                literalText.append(((Literal) p).getValue());
115
            } else {
116
                final String javaComp = Objects.requireNonNull(datePropToString.apply((DateProp) p));
117
                if (literalText.length() > 0) {
118
                    sb.append(literalToString.apply(literalText.toString()));
119
                    literalText.setLength(0);
120
                }
121
                sb.append(javaComp);
122
            }
123
        }
124
        if (literalText.length() > 0) {
125
            sb.append(literalToString.apply(literalText.toString()));
126
        }
127
        return sb.toString();
128
    }
129
 
130
    public static DateTimeFormatterBuilder createJavaFormatterBuilder(final List<? extends DateTimeFormatComponent> simpleFormat) {
131
        final DateTimeFormatterBuilder res = new DateTimeFormatterBuilder();
132
        for (final DateTimeFormatComponent p : simpleFormat) {
133
            if (p instanceof Literal) {
134
                res.appendLiteral(((Literal) p).getValue());
135
            } else {
136
                // from DateTimeFormatterBuilder.appendPattern()
137
                if (p == YEAR)
138
                    res.appendValue(ChronoField.YEAR, 4, 19, SignStyle.EXCEEDS_PAD);
139
                else if (p == MONTH_NAME)
140
                    res.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL);
141
                else if (p == MONTH_NUMBER)
142
                    res.appendValue(ChronoField.MONTH_OF_YEAR, 2);
143
                else if (p == DAY_IN_MONTH)
144
                    res.appendValue(ChronoField.DAY_OF_MONTH, 2);
145
                else if (p == DAY_NAME_IN_WEEK)
146
                    res.appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL);
147
                else if (p == HOUR)
148
                    res.appendValue(ChronoField.HOUR_OF_DAY, 2);
149
                else if (p == MINUTE)
150
                    res.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
151
                else if (p == SECOND)
152
                    res.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
153
                else if (p == MICROSECOND)
154
                    res.appendValue(ChronoField.MICRO_OF_SECOND, 6);
155
                else
156
                    throw new IllegalArgumentException("Unknown " + p);
157
            }
158
        }
159
        return res;
160
    }
161
 
162
    /**
163
     * Return the best pattern matching the input skeleton.
164
     *
165
     * @param simpleFormat the fields needed, e.g. [YEAR, DAY_IN_MONTH, MONTH_NUMBER].
166
     * @param l the locale needed.
167
     * @return the best match, e.g. "dd/MM/yyyy" for {@link Locale#FRANCE} , "MM/dd/yyyy" for
168
     *         {@link Locale#US}.
169
     */
170
    public static String getBestJavaPattern(final List<DateProp> simpleFormat, final Locale l) {
171
        final StringBuilder sb = new StringBuilder(128);
172
        for (final DateProp p : simpleFormat) {
173
            final String javaComp = JAVA_DATE_SPECS_PURE.get(p);
174
            if (javaComp != null)
175
                sb.append(javaComp);
176
            else
177
                throw new IllegalArgumentException("Unsupported spec : " + p);
178
        }
179
        // needs same length so our pattern works
180
        return DateTimePatternGenerator.getInstance(l).getBestPattern(sb.toString(), DateTimePatternGenerator.MATCH_ALL_FIELDS_LENGTH);
181
    }
182
 
183
    /**
184
     * Return the best pattern matching the input skeleton.
185
     *
186
     * @param simpleFormat the fields needed, e.g. [YEAR, DAY_IN_MONTH, MONTH_NUMBER].
187
     * @param l the locale needed.
188
     * @return the best match, e.g. [DAY_IN_MONTH, "/", MONTH_NUMBER, "/", YEAR] for
189
     *         {@link Locale#FRANCE} , [MONTH_NUMBER, "/", DAY_IN_MONTH, "/", YEAR] for
190
     *         {@link Locale#US}.
191
     */
192
    public static DateTimeFormat getBestPattern(final List<DateProp> simpleFormat, final Locale l) {
193
        return parseJavaPattern(getBestJavaPattern(simpleFormat, l));
194
    }
195
 
196
    static DateTimeFormat parseJavaPattern(final String bestPattern) {
197
        final Matcher matcher = REVERSE_SPEC_PATTERN.matcher(bestPattern);
198
        final Matcher quotedMatcher = StringUtils.SINGLE_QUOTED_PATTERN.matcher(bestPattern);
199
        final DateTimeFormatBuilder res = new DateTimeFormatBuilder();
200
        int index = 0;
201
        while (index < bestPattern.length()) {
202
            final int quoteIndex = bestPattern.indexOf('\'', index);
203
            final int endSansQuote = quoteIndex < 0 ? bestPattern.length() : quoteIndex;
204
 
205
            // parse quote-free string
206
            matcher.region(index, endSansQuote);
207
            while (matcher.find()) {
208
                if (index < matcher.start())
209
                    res.addLiteral(bestPattern.substring(index, matcher.start()));
210
                res.add(REVERSE_JAVA_SPEC.get(matcher.group()));
211
                index = matcher.end();
212
            }
213
            assert index <= endSansQuote : "region() failed";
214
            if (index < endSansQuote)
215
                res.addLiteral(bestPattern.substring(index, endSansQuote));
216
            index = endSansQuote;
217
 
218
            // parse quoted string
219
            if (index < bestPattern.length()) {
220
                quotedMatcher.region(index, bestPattern.length());
221
                if (!quotedMatcher.find() || quotedMatcher.start() != quotedMatcher.regionStart())
222
                    throw new IllegalStateException("Quoted string error : " + bestPattern.substring(quoteIndex));
223
                res.addLiteral(StringUtils.unSingleQuote(quotedMatcher));
224
                index = quotedMatcher.end();
225
            }
226
        }
227
        return res.build();
228
    }
229
 
230
    private final String name;
231
 
232
    private DateProp(String name) {
233
        this.name = name;
234
    }
235
 
236
    public final String getName() {
237
        return this.name;
238
    }
239
 
240
    @Override
241
    public String toString() {
242
        return this.getClass().getSimpleName() + ' ' + this.getName();
243
    }
244
}