OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 180 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
25 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
25 ilm 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;
15
 
16
import java.math.BigDecimal;
17
import java.math.BigInteger;
182 ilm 18
import java.time.LocalDate;
19
import java.time.LocalDateTime;
20
import java.time.ZoneId;
80 ilm 21
import java.util.Arrays;
25 ilm 22
import java.util.Calendar;
80 ilm 23
import java.util.Collection;
24
import java.util.Collections;
81 ilm 25
import java.util.Date;
80 ilm 26
import java.util.GregorianCalendar;
27
import java.util.HashMap;
28
import java.util.List;
29
import java.util.Map;
81 ilm 30
import java.util.TimeZone;
180 ilm 31
import java.util.concurrent.TimeUnit;
25 ilm 32
 
33
import javax.xml.datatype.DatatypeConfigurationException;
73 ilm 34
import javax.xml.datatype.DatatypeConstants;
81 ilm 35
import javax.xml.datatype.DatatypeConstants.Field;
25 ilm 36
import javax.xml.datatype.DatatypeFactory;
37
import javax.xml.datatype.Duration;
38
 
180 ilm 39
import net.jcip.annotations.GuardedBy;
80 ilm 40
import net.jcip.annotations.Immutable;
41
 
25 ilm 42
public class TimeUtils {
180 ilm 43
 
44
    static public final int SECONDS_PER_MINUTE = 60;
45
    static public final int MINUTE_PER_HOUR = 60;
46
    static public final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTE_PER_HOUR;
47
 
48
    @GuardedBy("TimeUtils.class")
25 ilm 49
    static private DatatypeFactory typeFactory = null;
180 ilm 50
    static private final List<Field> FIELDS_LIST = Arrays.asList(DatatypeConstants.YEARS, DatatypeConstants.MONTHS, DatatypeConstants.DAYS, DatatypeConstants.HOURS, DatatypeConstants.MINUTES,
80 ilm 51
            DatatypeConstants.SECONDS);
180 ilm 52
    static private final List<Field> DATE_FIELDS, TIME_FIELDS;
25 ilm 53
 
80 ilm 54
    static {
55
        final int dayIndex = FIELDS_LIST.indexOf(DatatypeConstants.DAYS);
56
        DATE_FIELDS = Collections.unmodifiableList(FIELDS_LIST.subList(0, dayIndex + 1));
57
        TIME_FIELDS = Collections.unmodifiableList(FIELDS_LIST.subList(dayIndex + 1, FIELDS_LIST.size()));
58
    }
59
 
60
    public static List<Field> getAllFields() {
61
        return FIELDS_LIST;
62
    }
63
 
64
    /**
65
     * Get the fields for the date part.
66
     *
67
     * @return fields until {@link DatatypeConstants#DAYS} included.
68
     */
69
    public static List<Field> getDateFields() {
70
        return DATE_FIELDS;
71
    }
72
 
73
    /**
74
     * Get the fields for the time part.
75
     *
76
     * @return fields from {@link DatatypeConstants#HOURS}.
77
     */
78
    public static List<Field> getTimeFields() {
79
        return TIME_FIELDS;
80
    }
81
 
82
    private static Class<? extends Number> getFieldClass(final Field f) {
83
        return f == DatatypeConstants.SECONDS ? BigDecimal.class : BigInteger.class;
84
    }
85
 
180 ilm 86
    static public synchronized final DatatypeFactory getTypeFactory() {
25 ilm 87
        if (typeFactory == null)
88
            try {
89
                typeFactory = DatatypeFactory.newInstance();
90
            } catch (DatatypeConfigurationException e) {
91
                throw new IllegalStateException(e);
92
            }
93
        return typeFactory;
94
    }
95
 
80 ilm 96
    static private final <N extends Number> N getZeroIfNull(final Number n, final Class<N> clazz) {
97
        final Number res;
98
        if (n != null)
99
            res = n;
100
        else if (clazz == BigInteger.class)
101
            res = BigInteger.ZERO;
102
        else if (clazz == BigDecimal.class)
103
            res = BigDecimal.ZERO;
104
        else
105
            throw new IllegalArgumentException("Unknown class : " + n);
106
        return clazz.cast(res);
107
    }
108
 
109
    static private final <N extends Number> N getNullIfZero(final N n) {
110
        if (n == null)
111
            return null;
112
        final boolean isZero;
113
        if (n instanceof BigInteger)
114
            isZero = n.intValue() == 0;
115
        else
116
            isZero = ((BigDecimal) n).compareTo(BigDecimal.ZERO) == 0;
117
        return isZero ? null : n;
118
    }
119
 
25 ilm 120
    /**
73 ilm 121
     * Get non-null seconds with the the correct class.
122
     *
123
     * @param d a duration.
124
     * @return the seconds, never <code>null</code>.
125
     * @see Duration#getField(javax.xml.datatype.DatatypeConstants.Field)
126
     * @see Duration#getMinutes()
127
     */
128
    static public final BigDecimal getSeconds(final Duration d) {
80 ilm 129
        return getZeroIfNull(d.getField(DatatypeConstants.SECONDS), BigDecimal.class);
73 ilm 130
    }
131
 
132
    /**
25 ilm 133
     * Convert the time part of a calendar to a duration.
134
     *
135
     * @param cal a calendar, e.g. 23/12/2011 11:55:33.066 GMT+02.
136
     * @return a duration, e.g. P0Y0M0DT11H55M33.066S.
137
     */
138
    public final static Duration timePartToDuration(final Calendar cal) {
139
        final BigDecimal seconds = BigDecimal.valueOf(cal.get(Calendar.SECOND)).add(BigDecimal.valueOf(cal.get(Calendar.MILLISECOND)).movePointLeft(3));
140
        return getTypeFactory().newDuration(true, BigInteger.ZERO, BigInteger.ZERO, BigInteger.ZERO, BigInteger.valueOf(cal.get(Calendar.HOUR_OF_DAY)), BigInteger.valueOf(cal.get(Calendar.MINUTE)),
141
                seconds);
142
    }
143
 
80 ilm 144
    // removes explicit 0
145
    public final static Duration trimDuration(final Duration dur) {
146
        return DurationNullsChanger.ALL_TO_NULL.apply(dur);
147
    }
148
 
149
    // replace null by 0
150
    public final static Duration removeNulls(final Duration dur) {
151
        return DurationNullsChanger.NONE_TO_NULL.apply(dur);
152
    }
153
 
154
    public static enum EmptyFieldPolicy {
155
        AS_IS, SET_TO_NULL, SET_TO_ZERO
156
    }
157
 
158
    public final static class DurationNullsBuilder {
159
 
160
        private final Map<Field, EmptyFieldPolicy> policy;
161
 
162
        public DurationNullsBuilder() {
163
            this(EmptyFieldPolicy.AS_IS);
164
        }
165
 
166
        public DurationNullsBuilder(final EmptyFieldPolicy initialPolicy) {
167
            this.policy = new HashMap<Field, EmptyFieldPolicy>();
168
            this.setPolicy(FIELDS_LIST, initialPolicy);
169
        }
170
 
171
        public final void setPolicy(Collection<Field> fields, final EmptyFieldPolicy to) {
172
            for (final Field f : fields)
173
                this.policy.put(f, to);
174
        }
175
 
176
        public final DurationNullsBuilder setToNull(Collection<Field> fields) {
177
            setPolicy(fields, EmptyFieldPolicy.SET_TO_NULL);
178
            return this;
179
        }
180
 
181
        public final DurationNullsBuilder setToZero(Collection<Field> fields) {
182
            setPolicy(fields, EmptyFieldPolicy.SET_TO_ZERO);
183
            return this;
184
        }
185
 
186
        public final DurationNullsBuilder dontChange(Collection<Field> fields) {
187
            setPolicy(fields, EmptyFieldPolicy.AS_IS);
188
            return this;
189
        }
190
 
191
        public final DurationNullsChanger build() {
192
            return new DurationNullsChanger(this.policy);
193
        }
194
    }
195
 
25 ilm 196
    /**
80 ilm 197
     * Allow to change empty fields between two equivalent state. In a {@link Duration} an empty
198
     * field can be set to <code>null</code> and it won't be output or it can be set to 0 and it
199
     * will be explicitly output.
200
     *
201
     * @author Sylvain
202
     * @see DurationNullsBuilder
203
     */
204
    @Immutable
205
    public final static class DurationNullsChanger {
206
 
207
        public final static DurationNullsChanger ALL_TO_NULL = new DurationNullsBuilder(EmptyFieldPolicy.SET_TO_NULL).build();
208
        public final static DurationNullsChanger NONE_TO_NULL = new DurationNullsBuilder(EmptyFieldPolicy.SET_TO_ZERO).build();
209
 
210
        private final Map<Field, EmptyFieldPolicy> policy;
211
 
212
        private DurationNullsChanger(final Map<Field, EmptyFieldPolicy> policy) {
213
            this.policy = Collections.unmodifiableMap(new HashMap<Field, EmptyFieldPolicy>(policy));
214
        }
215
 
216
        // doesn't change the duration value, just nulls and 0s
217
        public final Duration apply(final Duration dur) {
218
            boolean changed = false;
219
            final Map<Field, Number> newValues = new HashMap<Field, Number>();
220
            for (final Field f : FIELDS_LIST) {
221
                final Number oldVal = dur.getField(f);
222
                final EmptyFieldPolicy pol = this.policy.get(f);
223
                final Number newVal;
224
                if (pol == EmptyFieldPolicy.SET_TO_NULL) {
225
                    newVal = getNullIfZero(oldVal);
226
                } else if (pol == EmptyFieldPolicy.SET_TO_ZERO) {
227
                    newVal = getZeroIfNull(oldVal, getFieldClass(f));
228
                } else {
229
                    assert pol == EmptyFieldPolicy.AS_IS;
230
                    newVal = oldVal;
231
                }
232
                newValues.put(f, newVal);
233
                changed |= !CompareUtils.equals(newVal, oldVal);
234
            }
235
 
236
            if (!changed) {
237
                // Duration is immutable
238
                return dur;
239
            } else {
240
                return getTypeFactory().newDuration(dur.getSign() >= 0, (BigInteger) newValues.get(DatatypeConstants.YEARS), (BigInteger) newValues.get(DatatypeConstants.MONTHS),
241
                        (BigInteger) newValues.get(DatatypeConstants.DAYS), (BigInteger) newValues.get(DatatypeConstants.HOURS), (BigInteger) newValues.get(DatatypeConstants.MINUTES),
242
                        (BigDecimal) newValues.get(DatatypeConstants.SECONDS));
243
            }
244
        }
245
    }
246
 
247
    /**
25 ilm 248
     * Normalize <code>cal</code> so that any Calendar with the same local time have the same
249
     * result. If you don't need a Calendar this is faster than
250
     * {@link #copyLocalTime(Calendar, Calendar)}.
251
     *
252
     * @param cal a calendar, e.g. 0:00 CEST.
253
     * @return the time in millisecond of the UTC calendar with the same local time, e.g. 0:00 UTC.
254
     */
255
    public final static long normalizeLocalTime(final Calendar cal) {
256
        return cal.getTimeInMillis() + cal.getTimeZone().getOffset(cal.getTimeInMillis());
257
    }
258
 
259
    /**
260
     * Copy the local time from one calendar to another. Except if both calendars have the same time
261
     * zone, from.getTimeInMillis() will be different from to.getTimeInMillis().
80 ilm 262
     * <p>
263
     * NOTE : In case the two calendars are not from the same class but one of them is a
264
     * {@link GregorianCalendar} then this method will use a GregorianCalendar with the time zone
265
     * and absolute time of the other.
266
     * </p>
81 ilm 267
     * <p>
268
     * Also note that the local time of <code>from</code> can be {@link #isAmbiguous(Calendar)
269
     * ambiguous} in <code>to</code> or skipped. In the former case, the absolute time is
270
     * unspecified (e.g. 2h30 in WET can either be 2h30 CEST or CET in fall). In the latter case, a
271
     * round trip back to <code>from</code> will yield a different local time (e.g. in spring 2h30
272
     * in WET to 3h30 in CEST, back to 3h30 in WEST).
273
     * </p>
25 ilm 274
     *
275
     * @param from the source calendar, e.g. 23/12/2011 11:55:33.066 GMT-12.
276
     * @param to the destination calendar, e.g. 01/01/2000 0:00 GMT+13.
277
     * @return the modified destination calendar, e.g. 23/12/2011 11:55:33.066 GMT+13.
80 ilm 278
     * @throws IllegalArgumentException if both calendars aren't from the same class and none of
279
     *         them are Gregorian.
25 ilm 280
     */
80 ilm 281
    public final static Calendar copyLocalTime(final Calendar from, final Calendar to) throws IllegalArgumentException {
282
        final boolean sameClass = from.getClass() == to.getClass();
283
        final boolean createGregSource = !sameClass && to.getClass() == GregorianCalendar.class;
284
        final boolean createGregDest = !sameClass && from.getClass() == GregorianCalendar.class;
285
        if (!sameClass && !createGregSource && !createGregDest)
286
            throw new IllegalArgumentException("Calendars mismatch " + from.getClass() + " != " + to.getClass());
287
 
288
        final Calendar source = createGregSource ? new GregorianCalendar(from.getTimeZone()) : from;
289
        if (createGregSource) {
290
            source.setTime(from.getTime());
25 ilm 291
        }
80 ilm 292
        final Calendar dest = createGregDest ? new GregorianCalendar(to.getTimeZone()) : to;
293
        assert source.getClass() == dest.getClass();
294
        if (source.getTimeZone().equals(dest.getTimeZone())) {
295
            dest.setTimeInMillis(source.getTimeInMillis());
296
        } else {
297
            dest.clear();
298
            for (final int field : new int[] { Calendar.ERA, Calendar.YEAR, Calendar.DAY_OF_YEAR, Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND }) {
299
                dest.set(field, source.get(field));
300
            }
301
        }
302
        if (createGregDest) {
303
            to.setTime(dest.getTime());
304
        }
25 ilm 305
        return to;
306
    }
81 ilm 307
 
308
    /**
309
     * Whether the wall time of the passed calendar is ambiguous (i.e. happens twice the same day).
310
     * E.g. in France, Sun Oct 27 02:30 CEST 2013 then one hour later Sun Oct 27 02:30 CET 2013.
311
     *
312
     * @param cal a calendar.
313
     * @return <code>true</code> if the wall time (without time zone) is ambiguous.
314
     */
315
    public final static boolean isAmbiguous(final Calendar cal) {
316
        return isAmbiguous(getDSTChange(cal));
317
    }
318
 
319
    public final static boolean isAmbiguous(final DSTChange dstChange) {
320
        return dstChange == DSTChange.BEFORE_WINTER || dstChange == DSTChange.AFTER_WINTER;
321
    }
322
 
323
    /**
324
     * Relation to a DST change. Useful to test corner cases.
325
     *
326
     * @author Sylvain
327
     */
328
    static public enum DSTChange {
329
        /**
330
         * No change nearby.
331
         */
332
        NO,
333
        /**
334
         * DST will come, i.e. the next hour will be skipped.
335
         */
336
        BEFORE_SUMMER,
337
        /**
338
         * DST just came, i.e. the previous hour was skipped.
339
         */
340
        AFTER_SUMMER,
341
        /**
342
         * DST will go, the current hour will be happen again this day.
343
         */
344
        BEFORE_WINTER,
345
        /**
346
         * DST just went, the current wall time has already happened this day.
347
         */
348
        AFTER_WINTER
349
    }
350
 
351
    public final static DSTChange getDSTChange(final Calendar cal) {
352
        final TimeZone tz = cal.getTimeZone();
353
        if (!tz.useDaylightTime())
354
            return DSTChange.NO;
355
 
356
        final long currentMillis = cal.getTimeInMillis();
357
        final int hourInMillis = tz.getDSTSavings();
358
        final boolean isSTBefore = tz.inDaylightTime(new Date(currentMillis - hourInMillis));
359
        final boolean isST = tz.inDaylightTime(new Date(currentMillis));
360
        final boolean isSTAfter = tz.inDaylightTime(new Date(currentMillis + hourInMillis));
361
        if (isSTBefore != isST) {
362
            // only one change
363
            assert isST == isSTAfter;
364
            return isSTBefore ? DSTChange.AFTER_WINTER : DSTChange.AFTER_SUMMER;
365
        } else if (isST != isSTAfter) {
366
            // only one change
367
            assert isST == isSTBefore;
368
            return isSTAfter ? DSTChange.BEFORE_SUMMER : DSTChange.BEFORE_WINTER;
369
        } else {
370
            return DSTChange.NO;
371
        }
372
    }
90 ilm 373
 
374
    static public final Calendar clearTime(final Calendar cal) {
375
        // reset doesn't work, see javadoc
376
        cal.set(Calendar.HOUR_OF_DAY, 0);
377
        cal.clear(Calendar.MINUTE);
378
        cal.clear(Calendar.SECOND);
379
        cal.clear(Calendar.MILLISECOND);
380
        return cal;
381
    }
149 ilm 382
 
383
    /**
384
     * Whether 2 dates are in the same day.
385
     *
386
     * @param date1 the first date, it won't be modified (it will be {@link Calendar#clone()
387
     *        cloned}).
388
     * @param date2 the second date.
389
     * @return <code>true</code> if both dates are in the same day according to the passed calendar.
390
     */
391
    static public final boolean isSameDay(final Calendar date1, final Date date2) {
392
        return isSameDay((Calendar) date1.clone(), date1.getTime(), date2);
393
    }
394
 
395
    /**
396
     * Whether 2 dates are in the same day.
397
     *
398
     * @param cal the calendar to use, it will be modified.
399
     * @param date1 the first date.
400
     * @param date2 the second date.
401
     * @return <code>true</code> if both dates are in the same day according to the passed calendar.
402
     */
403
    static public final boolean isSameDay(final Calendar cal, final Date date1, final Date date2) {
404
        cal.setTime(date1);
405
        TimeUtils.clearTime(cal);
406
        final long day1 = cal.getTimeInMillis();
407
        cal.setTime(date2);
408
        TimeUtils.clearTime(cal);
409
        final long day2 = cal.getTimeInMillis();
410
        return day1 == day2;
411
    }
180 ilm 412
 
413
    static public final boolean isEqual(final long amount1, final TimeUnit unit1, final long amount2, final TimeUnit unit2) {
414
        final long finerAmount, coarserAmount;
415
        final TimeUnit finer, coarser;
416
        // don't truncate
417
        if (unit1.compareTo(unit2) < 0) {
418
            finerAmount = amount1;
419
            finer = unit1;
420
            coarserAmount = amount2;
421
            coarser = unit2;
422
        } else {
423
            finerAmount = amount2;
424
            finer = unit2;
425
            coarserAmount = amount1;
426
            coarser = unit1;
427
        }
428
        return finerAmount == finer.convert(coarserAmount, coarser);
429
    }
182 ilm 430
 
431
    public static LocalDateTime toLocalDateTime(Calendar calendar) {
432
        if (calendar == null)
433
            return null;
434
        return LocalDateTime.ofInstant(calendar.toInstant(), getTZ(calendar));
435
    }
436
 
437
    private static ZoneId getTZ(Calendar calendar) {
438
        final TimeZone tz = calendar.getTimeZone();
439
        return tz == null ? ZoneId.systemDefault() : tz.toZoneId();
440
    }
441
 
442
    public static final Calendar toCalendar(final LocalDateTime dt) {
443
        final Calendar cal = Calendar.getInstance();
444
        cal.setTimeInMillis(dt.atZone(getTZ(cal)).toInstant().toEpochMilli());
445
        return cal;
446
    }
447
 
448
    public static LocalDate toLocalDate(Calendar calendar) {
449
        if (calendar == null)
450
            return null;
451
        if (calendar instanceof GregorianCalendar)
452
            return LocalDate.of(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH));
453
        else
454
            return toLocalDateTime(calendar).toLocalDate();
455
    }
25 ilm 456
}