OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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