001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 The ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package org.conqat.lib.commons.date;
018
019import java.text.ParseException;
020import java.text.SimpleDateFormat;
021import java.time.Duration;
022import java.time.Instant;
023import java.time.LocalDateTime;
024import java.time.ZoneId;
025import java.time.ZonedDateTime;
026import java.time.format.DateTimeFormatter;
027import java.util.ArrayList;
028import java.util.Calendar;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.Date;
032import java.util.List;
033import java.util.Locale;
034import java.util.concurrent.TimeUnit;
035
036import org.conqat.lib.commons.assertion.CCSMAssert;
037import org.conqat.lib.commons.error.NeverThrownRuntimeException;
038import org.conqat.lib.commons.factory.IFactory;
039
040/**
041 * Utility methods for working on date objects.
042 */
043public class DateUtils {
044
045        /** The number of milliseconds per second */
046        public static final long MILLIS_PER_SECOND = 1000;
047
048        /** The number of nanoseconds per second */
049        public static final long NANOS_PER_SECOND = MILLIS_PER_SECOND * 1000 * 1000;
050
051        /** The number of milliseconds per minute */
052        public static final long MILLIS_PER_MINUTE = 1000 * 60;
053
054        /** The number of milliseconds per hour */
055        public static final long MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60;
056
057        /** The number of milliseconds per day */
058        public static final long MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
059
060        /** Simple date format, which matches the UI format */
061        private static final SimpleDateFormat MMM_DD_YYYY_HH_MM_FORMAT = new SimpleDateFormat("MMM dd yyyy HH:mm",
062                        Locale.ENGLISH);
063
064        /** Simple date format used by {@link #truncateToBeginOfDay(Date)} */
065        private static final SimpleDateFormat YYYY_MM_DD_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
066
067        /** The factory used to create the date in {@link #getNow()}. */
068        private static IFactory<Date, NeverThrownRuntimeException> nowFactory;
069
070        /** Returns the latest date in a collection of dates */
071        public static Date getLatest(Collection<Date> dates) {
072                if (dates.isEmpty()) {
073                        return null;
074                }
075                return Collections.max(dates);
076        }
077
078        /** Returns the earliest date in a collection of dates */
079        public static Date getEarliest(Collection<Date> dates) {
080                if (dates.isEmpty()) {
081                        return null;
082                }
083                return Collections.min(dates);
084        }
085
086        /**
087         * Returns the earlier of two dates, or null, if one of the dates is null
088         */
089        public static Date min(Date d1, Date d2) {
090                if (d1 == null || d2 == null) {
091                        return null;
092                }
093
094                if (d1.compareTo(d2) < 0) {
095                        return d1;
096                }
097                return d2;
098        }
099
100        /** Returns the later of two dates or null, if one of the dates is null. */
101        public static Date max(Date d1, Date d2) {
102                if (d1 == null || d2 == null) {
103                        return null;
104                }
105
106                if (d2.compareTo(d1) > 0) {
107                        return d2;
108                }
109                return d1;
110        }
111
112        /**
113         * Retrieves the date which is exactly the given number of days before now.
114         */
115        public static Date getDateBefore(int days) {
116                Calendar calendar = Calendar.getInstance();
117                calendar.add(Calendar.DAY_OF_YEAR, -days);
118                return calendar.getTime();
119        }
120
121        /**
122         * Returns the current time as a {@link Date} object. This is preferred to
123         * directly calling the {@link Date#Date()} constructor, as this method provides
124         * a central entry point that allows the notion of time to be tweaked (e.g. for
125         * testing, when we want a certain date to be returned).
126         *
127         * The behavior of this method can be affected either by calling the
128         * {@link #testcode_setNowFactory(IFactory)} method, or by providing a fixed
129         * date in format "yyyyMMddHHmmss" as the system property
130         * "org.conqat.lib.commons.date.now".
131         */
132        public static synchronized Date getNow() {
133                if (nowFactory == null) {
134                        String property = System.getProperty("org.conqat.lib.commons.date.now");
135                        if (property == null) {
136                                nowFactory = new CurrentDateFactory();
137                        } else {
138                                try {
139                                        nowFactory = new FixedDateFactory(new SimpleDateFormat("yyyyMMddHHmmss").parse(property));
140                                } catch (ParseException e) {
141                                        // fail hard in case of misconfiguration
142                                        throw new RuntimeException("Invalid date string provided via system property: " + property, e);
143                                }
144                        }
145                }
146                return nowFactory.create();
147        }
148
149        /**
150         * {@link #getNow()} truncated to the begin of day.
151         */
152        public static Date getNowWithoutTime() {
153                return truncateToBeginOfDay(getNow());
154        }
155
156        /**
157         * Returns the factory that affects the notion of "now" in {@link #getNow()} .
158         * This should be only called from test code!
159         */
160        public static synchronized IFactory<Date, NeverThrownRuntimeException> testcode_getNowFactory() {
161                return DateUtils.nowFactory;
162        }
163
164        /**
165         * Sets the factory that affects the notion of "now" in {@link #getNow()}. This
166         * should be only called from test code!
167         */
168        public static synchronized void testcode_setNowFactory(IFactory<Date, NeverThrownRuntimeException> nowFactory) {
169                DateUtils.nowFactory = nowFactory;
170        }
171
172        /**
173         * Sets the given fixed date for "now" in {@link #getNow()}. This should be only
174         * called from test code! It is absolutely necessary to call the reset method
175         * afterwards in order to prevent test interactions over the statically shared
176         * {@link #nowFactory}.
177         */
178        public static synchronized void testcode_setFixedDate(Date date) {
179                testcode_setNowFactory(new FixedDateFactory(date));
180        }
181
182        /**
183         * Reset the now factory to the default one. It is also a good idea to reset the
184         * license manager after calling this, if the test case might have created a new
185         * global instance of the license manager after setting a fixed date (because
186         * chances are that the date influences the validity of the used license).
187         */
188        public static synchronized void testcode_resetNowFactory() {
189                DateUtils.nowFactory = null;
190        }
191
192        /** A factory returning a fixed date. */
193        public static class FixedDateFactory implements IFactory<Date, NeverThrownRuntimeException> {
194
195                /** The date used as now. */
196                private final Date now;
197
198                /** Constructor. */
199                public FixedDateFactory(Date now) {
200                        this.now = now;
201                }
202
203                /** {@inheritDoc} */
204                @Override
205                public Date create() throws NeverThrownRuntimeException {
206                        return (Date) now.clone();
207                }
208        }
209
210        /** A factory returning the current date. */
211        public static class CurrentDateFactory implements IFactory<Date, NeverThrownRuntimeException> {
212
213                /** {@inheritDoc} */
214                @Override
215                public Date create() throws NeverThrownRuntimeException {
216                        return new Date();
217                }
218        }
219
220        /** Returns a new Date that is one day later than the given date. */
221        public static Date incrementByOneDay(Date date) {
222                return addDaysToDate(date, 1);
223        }
224
225        /**
226         * Returns a normalized version of the given date. Normalization is done by
227         * removing all time-of-day information.
228         */
229        public static Date truncateToBeginOfDay(Date date) {
230                if (date == null) {
231                        return null;
232                }
233
234                synchronized (YYYY_MM_DD_FORMAT) {
235                        String normalized = YYYY_MM_DD_FORMAT.format(date);
236                        try {
237                                return YYYY_MM_DD_FORMAT.parse(normalized);
238                        } catch (ParseException e) {
239                                throw new AssertionError("ParseException.", e);
240                        }
241                }
242        }
243
244        /**
245         * Formats the given timestamp as date YYYY-MM-DD
246         */
247        public static String formatTimestampToDate(long timestamp) {
248                synchronized (YYYY_MM_DD_FORMAT) {
249                        return YYYY_MM_DD_FORMAT.format(new Date(timestamp));
250                }
251        }
252
253        /** Converts the given number of days into milliseconds. */
254        public static long daysToMilliseconds(int days) {
255                return days * MILLIS_PER_DAY;
256        }
257
258        /** Converts the given number of hours into milliseconds. */
259        public static long hoursToMilliseconds(int hours) {
260                return hours * MILLIS_PER_HOUR;
261        }
262
263        /** Converts the given number of minutes into milliseconds. */
264        public static long minutesToMilliseconds(int minutes) {
265                return minutes * MILLIS_PER_MINUTE;
266        }
267
268        /**
269         * Returns the difference between two dates in the given time unit.
270         */
271        public static long diff(Date earlier, Date later, TimeUnit timeUnit) {
272                long diffInMillies = later.getTime() - earlier.getTime();
273                return timeUnit.convert(diffInMillies, TimeUnit.MILLISECONDS);
274        }
275
276        /**
277         * Returns the difference between two dates in days.
278         */
279        public static long diffDays(Date earlier, Date later) {
280                return diff(earlier, later, TimeUnit.DAYS);
281        }
282
283        /** Create a new date and add <code>days</code> to it. */
284        public static Date addDaysToDate(Date date, int days) {
285                Calendar calendar = Calendar.getInstance();
286                calendar.setTime(date);
287                calendar.add(Calendar.DAY_OF_YEAR, days);
288                return calendar.getTime();
289        }
290
291        /** Create a new date and subtracts <code>days</code> from it. */
292        public static Date subtractDaysFromDate(Date date, int days) {
293                return addDaysToDate(date, -days);
294        }
295
296        /**
297         * Get an ordered list of dates that includes <code>startDate</code>, date
298         * points in between start and end date and the <code>endDate</code> if
299         * <code>includeEndDate</code> is true.
300         */
301        public static List<Date> getDatePointsInTimespan(Date startDate, Date endDate, int daysBetweenDatePoints,
302                        boolean includeEndDate) {
303                CCSMAssert.isNotNull(startDate);
304                CCSMAssert.isNotNull(endDate);
305                CCSMAssert.isFalse(startDate.after(endDate), "startDate is after endDate");
306                CCSMAssert.isTrue(daysBetweenDatePoints > 0, "daysBetweenDatePoints must be > 0");
307
308                List<Date> result = new ArrayList<>();
309                result.add(startDate);
310
311                Date currentDate = startDate;
312
313                while (true) {
314                        currentDate = addDaysToDate(currentDate, daysBetweenDatePoints);
315
316                        if (!currentDate.before(endDate)) {
317                                break;
318                        }
319
320                        result.add(currentDate);
321                }
322
323                if (includeEndDate) {
324                        result.add(endDate);
325                }
326
327                return result;
328        }
329
330        /**
331         * Get the timestamp of a date. Returns null if <code>date</code> is null.
332         */
333        public static Long getTimeOfDate(Date date) {
334                if (date == null) {
335                        return null;
336                }
337
338                return date.getTime();
339        }
340
341        /**
342         * Create a date.
343         *
344         * @param month
345         *            zero-based index
346         */
347        public static Date createDate(int day, int month, int year) {
348                if (month == 12) {
349                        throw new IllegalArgumentException("month is zero-based, use the Calendar instances");
350                }
351
352                Calendar calendar = Calendar.getInstance();
353                calendar.set(year, month, day);
354                return DateUtils.truncateToBeginOfDay(calendar.getTime());
355        }
356
357        /**
358         * Takes a <code>timestamp</code> in milliseconds, creates a date based on this
359         * timestamp and returns the UI formatted String representation of this date.
360         * This used a fixed English locale.
361         */
362        public static String getUiFormattedDateString(long milliseconds) {
363                Date date = new Date(milliseconds);
364                synchronized (MMM_DD_YYYY_HH_MM_FORMAT) {
365                        return MMM_DD_YYYY_HH_MM_FORMAT.format(date);
366                }
367        }
368
369        /**
370         * Takes a <code>timestamp</code> in milliseconds and returns formatted string
371         * representation of date using given pattern. The pattern format is the one
372         * used in Java {@link DateTimeFormatter}. Uses English locale.
373         */
374        public static String formatDate(long timestamp, String pattern) {
375                LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
376                DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, Locale.ENGLISH);
377                return localDateTime.format(formatter);
378        }
379
380        /** Formats a duration in a human-readable way (e.g. 1h 20min 5s 123ms). */
381        public static String formatDurationHumanReadable(Duration duration) {
382                long milliseconds = duration.toMillis() % 1000;
383                long seconds = (duration.toMillis() / 1000) % 60;
384                long minutes = (duration.toMillis() / 1000 / 60) % 60;
385                long hours = (duration.toMillis() / 1000 / 60 / 60);
386
387                StringBuilder builder = new StringBuilder();
388                if (hours > 0) {
389                        builder.append(hours).append("h ");
390                }
391                if (hours > 0 || minutes > 0) {
392                        builder.append(minutes).append("min ");
393                }
394                if (hours > 0 || minutes > 0 || seconds > 0) {
395                        builder.append(seconds).append("s ");
396                }
397                builder.append(milliseconds).append("ms");
398                return builder.toString();
399        }
400
401        /** Converts nano seconds to seconds. */
402        public static double nanosToSeconds(long nanos) {
403                return nanos / 1_000_000_000d;
404        }
405
406        /** Creates a new {@link ZonedDateTime} for the given timestamp. */
407        public static ZonedDateTime createZonedDateTimeForTimestamp(long timestamp) {
408                return ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
409        }
410
411}