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}