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 |
}
|