View Javadoc

1   package com.atlassian.core.util;
2   
3   import com.opensymphony.util.TextUtils;
4   import org.apache.log4j.Category;
5   
6   import java.math.BigDecimal;
7   import java.sql.Timestamp;
8   import java.text.DateFormat;
9   import java.text.SimpleDateFormat;
10  import java.util.Calendar;
11  import java.util.Date;
12  import java.util.MissingResourceException;
13  import java.util.ResourceBundle;
14  import java.util.StringTokenizer;
15  import java.util.regex.Matcher;
16  import java.util.regex.Pattern;
17  
18  public class DateUtils
19  {
20      private static Category log = Category.getInstance(DateUtils.class);
21      // starts with some digits (e.g. "22"). optionally has a dot followed by more digits (e.g. ".45"). or starts
22      // with a dot followed by multiple digits (".25"). finally there is other "stuff" which is the unit of time
23      // (e.g. "d" or "hours").
24      // Good = 24h 35.5 0.2h .8h
25      // Bad = 24.h -23h
26      private static final Pattern DURATION_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?|\\.\\d+)(.+)");
27  
28      public enum Duration
29      {
30          SECOND(1),
31          MINUTE(60),
32          HOUR(60 * MINUTE.getSeconds()),
33          DAY(24 * HOUR.getSeconds())
34                  {
35                      @Override
36                      public long getModifiedSeconds(final long secondsPerDay, final long secondsPerWeek)
37                      {
38                          return secondsPerDay;
39                      }
40                  },
41          WEEK(7 * DAY.getSeconds())
42                  {
43                      @Override
44                      public long getModifiedSeconds(final long secondsPerDay, final long secondsPerWeek)
45                      {
46                          return secondsPerWeek;
47                      }
48                  },
49          MONTH(31 * DAY.getSeconds())
50                  {
51                      // since a month has 31 days in it, clearly you shouldn't be using this and expecting precision.
52                      // stick with the ones above.
53                      @Override
54                      public long getModifiedSeconds(final long secondsPerDay, final long secondsPerWeek)
55                      {
56                          return 31 * secondsPerDay;
57                      }
58                  },
59          YEAR(52 * WEEK.getSeconds())
60                  {
61                      // as with MONTH above, you shouldn't expect precision when you use YEAR. In particular a year
62                      // has 52 weeks...which is NOT the same as 365 days with every 4th year having 366.
63                      @Override
64                      public long getModifiedSeconds(final long secondsPerDay, final long secondsPerWeek)
65                      {
66                          return 52 * secondsPerWeek;
67                      }
68                  };
69  
70          public long getSeconds()
71          {
72              return seconds;
73          }
74  
75          public long getMilliseconds()
76          {
77              return 1000L * getSeconds();
78          }
79  
80          /**
81           * Sometimes customers configure the meaning of "day" or "week" to mean something like "1 day = 8 hours".
82           * @param secondsPerDay how many seconds are in a "day"
83           * @param secondsPerWeek how many seconds are in a "week" (based on number of days per week and hours per day)
84           * @return number of seconds in the duration, taking into account the modified definition of "day" and "week"
85           */
86          public long getModifiedSeconds(final long secondsPerDay, final long secondsPerWeek)
87          {
88              return getSeconds();
89          }
90  
91          private final long seconds;
92  
93          private Duration(final long seconds)
94          {
95              this.seconds = seconds;
96          }
97      }
98  
99      // these are obsoleted by the above but we need to keep them for backwards compatability
100     public static final long SECOND_MILLIS = Duration.SECOND.getMilliseconds();
101     public static final long MINUTE_MILLIS = Duration.MINUTE.getMilliseconds();
102     public static final long HOUR_MILLIS = Duration.HOUR.getMilliseconds();
103     public static final long DAY_MILLIS = Duration.DAY.getMilliseconds();
104     public static final long MONTH_MILLIS = Duration.MONTH.getMilliseconds();
105     public static final long YEAR_MILLIS = Duration.YEAR.getMilliseconds();
106 
107     public static final String AM = "am";
108     public static final String PM = "pm";
109 
110     /*
111        It's important these stay ordered from biggest to smallest.
112        Everything in this must be able to be zeroed without changing anything
113        that comes before it in the list. So don't add WEEK!
114      */
115     private static final int[] CALENDAR_PERIODS = { Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH,
116                                                     Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND };
117 
118     // This is used by the Velocity templates as a bean
119     private final ResourceBundle resourceBundle;
120 
121     public static class DateRange
122     {
123         public final java.util.Date startDate;
124         public final java.util.Date endDate;
125 
126         public DateRange(java.util.Date startDate, java.util.Date endDate)
127         {
128             this.startDate = startDate;
129             this.endDate = endDate;
130         }
131     }
132 
133 
134     public DateUtils(ResourceBundle resourceBundle)
135     {
136         this.resourceBundle = resourceBundle;
137     }
138 
139     /** compares if these two timestamps are within 10 milliseconds of each other (precision error)
140      * @param t1 first timestamp to compare
141      * @param t2 second timestamp to compare
142      * @return true if the two timestamps are within 10 milliseconds of one another
143      */
144     public static boolean equalTimestamps(Timestamp t1, Timestamp t2)
145     {
146         return (Math.abs(t1.getTime() - t2.getTime()) < 10L);
147     }
148 
149     /** Date Format to be used for internal logging operations */
150     public static final DateFormat ISO8601DateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
151 
152     public String dateDifferenceBean(long dateA, long dateB, long resolution, ResourceBundle resourceBundle)
153     {
154         return dateDifference(dateA, dateB, resolution, resourceBundle);
155     }
156 
157     /**
158      * Resolution is the degree of difference.
159      * <p/>
160      * 0 = months
161      * 1 = days
162      * 2 = hours
163      * 3 = minutes
164      * 4 = seconds
165      * @param dateA first date to compare
166      * @param dateB second date to compare
167      * @param resolution the degree of difference
168      * @param resourceBundle contains localizations for core.dateutils strings
169      * @return the difference between the two dates as a human readable string (i.e. 2 months, 3 days, 4 hours)
170      */
171     public static String dateDifference(long dateA, long dateB, long resolution, ResourceBundle resourceBundle)
172     {
173         long months, days, hours, minutes, seconds;
174         StringBuilder sb = new StringBuilder();
175         long difference = Math.abs(dateB - dateA);
176 
177         resolution--;
178         months = difference / Duration.MONTH.getMilliseconds();
179         if (months > 0)
180         {
181             difference = difference % Duration.MONTH.getMilliseconds();
182             if (months > 1)
183             {
184                 sb.append(months).append(" ").append(getText(resourceBundle, "core.dateutils.months")).append(", ");
185             }
186             else
187             {
188                 sb.append(months).append(" ").append(getText(resourceBundle, "core.dateutils.month")).append(", ");
189             }
190         }
191 
192         if (resolution < 0)
193         {
194             if (sb.length() == 0)
195             {
196                 return "0 " + getText(resourceBundle, "core.dateutils.months");
197             }
198             else
199             {
200                 return sb.substring(0, sb.length() - 2);
201             }
202         }
203         else
204         {
205             resolution--;
206             days = difference / Duration.DAY.getMilliseconds();
207             if (days > 0)
208             {
209                 difference = difference % Duration.DAY.getMilliseconds();
210                 if (days > 1)
211                 {
212                     sb.append(days).append(" ").append(getText(resourceBundle, "core.dateutils.days")).append(", ");
213                 }
214                 else
215                 {
216                     sb.append(days).append(" ").append(getText(resourceBundle, "core.dateutils.day")).append(", ");
217                 }
218             }
219         }
220 
221         if (resolution < 0)
222         {
223             if (sb.length() == 0)
224             {
225                 return "0 " + getText(resourceBundle, "core.dateutils.days");
226             }
227             else
228             {
229                 return sb.substring(0, sb.length() - 2);
230             }
231         }
232         else
233         {
234             resolution--;
235             hours = difference / Duration.HOUR.getMilliseconds();
236             if (hours > 0)
237             {
238                 difference = difference % Duration.HOUR.getMilliseconds();
239                 if (hours > 1)
240                 {
241                     sb.append(hours).append(" ").append(getText(resourceBundle, "core.dateutils.hours")).append(", ");
242                 }
243                 else
244                 {
245                     sb.append(hours).append(" ").append(getText(resourceBundle, "core.dateutils.hour")).append(", ");
246                 }
247             }
248         }
249 
250         if (resolution < 0)
251         {
252             if (sb.length() == 0)
253             {
254                 return "0 " + getText(resourceBundle, "core.dateutils.hours");
255             }
256             else
257             {
258                 return sb.substring(0, sb.length() - 2);
259             }
260         }
261         else
262         {
263             resolution--;
264             minutes = difference / Duration.MINUTE.getMilliseconds();
265             if (minutes > 0)
266             {
267                 difference = difference % Duration.MINUTE.getMilliseconds();
268                 if (minutes > 1)
269                 {
270                     sb.append(minutes).append(" ").append(getText(resourceBundle, "core.dateutils.minutes")).append(", ");
271                 }
272                 else
273                 {
274                     sb.append(minutes).append(" ").append(getText(resourceBundle, "core.dateutils.minute")).append(", ");
275                 }
276             }
277         }
278 
279         if (resolution < 0)
280         {
281             if (sb.length() == 0)
282             {
283                 return "0 " + getText(resourceBundle, "core.dateutils.minutes");
284             }
285             else
286             {
287                 return sb.substring(0, sb.length() - 2);
288             }
289         }
290         else
291         {
292             resolution--;
293             seconds = difference / Duration.SECOND.getMilliseconds();
294             if (seconds > 0)
295             {
296                 if (seconds > 1)
297                 {
298                     sb.append(seconds).append(" ").append(getText(resourceBundle, "core.dateutils.seconds")).append(", ");
299                 }
300                 else
301                 {
302                     sb.append(seconds).append(" ").append(getText(resourceBundle, "core.dateutils.second")).append(", ");
303                 }
304             }
305         }
306 
307         if (resolution <= 0 && sb.length() == 0)
308         {
309             return "0 " + getText(resourceBundle, "core.dateutils.seconds");
310         }
311 
312         if (sb.length() > 2)
313         {
314             return sb.substring(0, sb.length() - 2);
315         }
316         else
317         {
318             return "";
319         }
320     }
321 
322 
323     public static String formatDateISO8601(Date ts)
324     {
325         return ISO8601DateFormat.format(ts);
326     }
327 
328     /**
329      * Check whether a given duration string is valid
330      *
331      * @param s the duration string
332      * @return true if it a valid duration
333      */
334     public static boolean validDuration(String s)
335     {
336         try
337         {
338             getDuration(s);
339             return true;
340         }
341         catch (InvalidDurationException e)
342         {
343             return false;
344         }
345     }
346 
347     /**
348      * Given a duration string, get the number of seconds it represents (all case insensitive):
349      * <ul>
350      * <li>w = weeks
351      * <li>d = days
352      * <li>h = hours
353      * <li>m = minutes
354      * </ul>
355      * If no category is specified, assume minutes.<br>
356      * Each field must be separated by a space, and they can come in any order.
357      * Case is ignored.
358      * <p/>
359      * ie 2h = 7200, 60m = 3600, 3d = 259200, 30m
360      *
361      * @param durationStr the duration string
362      * @return the duration in seconds
363      * @throws InvalidDurationException if the duration is invalid
364      */
365     public static long getDuration(String durationStr) throws InvalidDurationException
366     {
367         return getDuration(durationStr, Duration.MINUTE);
368     }
369 
370     /**
371      * Given a duration string, get the number of seconds it represents (all case insensitive):
372      * <ul>
373      * <li>w = weeks
374      * <li>d = days
375      * <li>h = hours
376      * <li>m = minutes
377      * </ul>
378      * ie 2h = 7200, 60m = 3600, 3d = 259200, 30m
379      *
380      * @param durationStr the duration string
381      * @param defaultUnit the unit used when another is not specified in the durationStr
382      * @return the duration in seconds
383      * @throws InvalidDurationException if the duration is invalid
384      */
385     public static long getDuration(final String durationStr, final Duration defaultUnit) throws InvalidDurationException
386     {
387         return getDurationSeconds(durationStr, Duration.DAY.getSeconds(), Duration.WEEK.getSeconds(), defaultUnit);
388     }
389 
390     /**
391      * This function retrieves a duration in seconds that depends on number of hours in a day and
392      * days in a week. The default unit is MINUTE (i.e. "2" == "2 minutes")
393      *
394      * @param durationStr to convert to a duration
395      * @param hoursPerDay Number of hourse i day
396      * @param daysPerWeek Days Per Week
397      * @return the duration in seconds
398      * @throws InvalidDurationException if its badly formatted duration
399      */
400     public static long getDuration(String durationStr, int hoursPerDay, int daysPerWeek) throws InvalidDurationException
401     {
402         return getDuration(durationStr, hoursPerDay, daysPerWeek, Duration.MINUTE);
403     }
404 
405     /**
406      * This function retrieves a duration in seconds that depends on number of hours in a day and
407      * days in a week
408      *
409      * @param durationStr to convert to a duration
410      * @param hoursPerDay Number of hourse i day
411      * @param daysPerWeek Days Per Week
412      * @param defaultUnit the unit used when one is not specified on a measure in the durationStr
413      * @return the duration in seconds
414      * @throws InvalidDurationException if its badly formatted duration
415      */
416     public static long getDuration(String durationStr, int hoursPerDay, int daysPerWeek, final Duration defaultUnit) throws InvalidDurationException
417     {
418         long secondsInDay = hoursPerDay * Duration.HOUR.getSeconds();
419         long secondsPerWeek = daysPerWeek * secondsInDay;
420         return getDurationSeconds(durationStr, secondsInDay, secondsPerWeek, defaultUnit);
421     }
422 
423     /**
424      * Get a duration string with the possibility of a negative.
425      * <p/>
426      * A duration will be considered negative if the first non-space character is a - sign.
427      *
428      * @param durationStr the duration string
429      * @return the duration in seconds, which can be negative
430      * @throws InvalidDurationException if its a badly formatted duration
431      */
432     public static long getDurationWithNegative(String durationStr) throws InvalidDurationException
433     {
434         String cleanedDurationStr = TextUtils.noNull(durationStr).trim();
435         if (!TextUtils.stringSet(cleanedDurationStr))
436         {
437             return 0;
438         }
439 
440         boolean negative = false;
441 
442         if (cleanedDurationStr.charAt(0) == '-')
443         {
444             negative = true;
445         }
446 
447         if (negative)
448         {
449             return 0 - getDuration(cleanedDurationStr.substring(1));
450         }
451         else
452         {
453             return getDuration(cleanedDurationStr);
454         }
455     }
456 
457     /**
458      * Convert a duration string in the number of seconds it represents.
459      * This method takes seconds per day and seconds per weeks instead of "hours per day" or "days per week"
460      * because we may want a non-integral number of hours per day.
461      * @param durationStr the duration string
462      * @param secondsPerDay number of seconds in a working "day" (e.g. could be equal 6.5 hours)
463      * @param secondsPerWeek number of seconds in a working "week" (e.g. could be equal to 4.5 days)
464      * @param defaultUnit the unit to use for numbers with no unit specified (e.g. "12")
465      * @return the number of seconds representing the duration string
466      * @throws InvalidDurationException if the duration string cannot be parsed
467      */
468     public static long getDurationSeconds(String durationStr, long secondsPerDay, long secondsPerWeek, final Duration defaultUnit)
469             throws InvalidDurationException
470     {
471         long time = 0;
472 
473         if (org.apache.commons.lang.StringUtils.isBlank(durationStr))
474         {
475             return 0;
476         }
477 
478         durationStr = durationStr.trim().toLowerCase();
479 
480         // if we have more than one 'token', parse each separately
481         if (durationStr.indexOf(" ") > 0)
482         {
483             StringTokenizer st = new StringTokenizer(durationStr, ", ");
484             while (st.hasMoreTokens())
485             {
486                 time += getDurationSeconds(st.nextToken(), secondsPerDay, secondsPerWeek, defaultUnit);
487             }
488         }
489         else
490         {
491             try // detect if we just have a number
492             {
493                 time = Long.parseLong(durationStr.trim()) * defaultUnit.getModifiedSeconds(secondsPerDay, secondsPerWeek);
494             }
495             catch (Exception ex) // otherwise get the value
496             {
497                 final Matcher matcher = DURATION_PATTERN.matcher(durationStr);
498                 if(matcher.matches())
499                 {
500                     // the regex will have two groups. the "number part" and the "unit" part
501                     final String numberAsString = matcher.group(1);
502                     final BigDecimal number = new BigDecimal(numberAsString);
503 
504                     final long unit = getUnit(matcher.group(2), secondsPerDay, secondsPerWeek);
505                     final BigDecimal seconds = number.multiply(BigDecimal.valueOf(unit));
506                     try
507                     {
508                         // we track time in seconds but care about accuracy to the minute. if the decimal fraction
509                         // specified would require sub-minute accuracy to store then we want an InvalidDurationException.
510 
511                         // If we allowed second-accuracy then people could enter somewhat nonsensical times like "2.27 hours"
512                         // which we would gladly store as 2 hours, 16 minutes, and 2 seconds. But since we never display
513                         // the second part of the time a user would just see "2 hours, 16 minutes". "2.28 hours" would also
514                         // be displayed as "2 hours, 16 minutes". We want to preserve a 1:1 mapping between allowable decimal
515                         // fractions and pretty formatted durations.
516 
517                         // this call will trigger an exception if rounding would occur...we will propagate that
518                         // as an InvalidDurationException
519                         //noinspection ResultOfMethodCallIgnored
520                         seconds.divide(BigDecimal.valueOf(60)).intValueExact();
521 
522                         // if we got here then we are only storing minutes and not seconds.
523                         time = seconds.intValueExact();
524 
525                     }
526                     catch (ArithmeticException e)
527                     {
528                         throw new InvalidDurationException("Specified decimal fraction duration cannot maintain precision", e);
529                     }
530                 }
531                 else
532                 {
533                     throw new InvalidDurationException("Unable to parse duration string: " + durationStr);
534                 }
535             }
536         }
537         return time;
538     }
539 
540     /**
541      * Given a string such as "d" or "days" return the number of seconds in one of those units
542      * @param unit the string to investigate.
543      * @param secondsPerDay the number of seconds in a working day
544      * @param secondsPerWeek the number of seconds in a working week
545      * @return the number of seconds in the unit described by the string
546      * @throws InvalidDurationException if the string isn't a valid unit of time
547      */
548     private static long getUnit(final String unit, final long secondsPerDay, final long secondsPerWeek) throws InvalidDurationException
549     {
550         long time;
551         switch ((int) unit.charAt(0))
552         {
553             case(int) 'm':
554                 validateDurationUnit(unit.substring(0), Duration.MINUTE);
555                 time = Duration.MINUTE.getSeconds();
556                 break;
557             case(int) 'h':
558                 validateDurationUnit(unit.substring(0), Duration.HOUR);
559                 time = Duration.HOUR.getSeconds();
560                 break;
561             case(int) 'd':
562                 validateDurationUnit(unit.substring(0), Duration.DAY);
563                 time = secondsPerDay;
564                 break;
565             case(int) 'w':
566                 validateDurationUnit(unit.substring(0), Duration.WEEK);
567                 time = secondsPerWeek;
568                 break;
569             default:
570                 throw new InvalidDurationException("Not a valid duration string");
571         }
572         return time;
573     }
574 
575     private static String validateDurationUnit(final String durationString, final Duration duration)
576             throws InvalidDurationException
577     {
578 
579         if (durationString.length() > 1)
580         {
581             String singular = duration.name().toLowerCase();
582             String plural = duration.name().toLowerCase() + "s";
583 
584             if (durationString.contains(plural))
585             {
586                 return durationString.substring(durationString.indexOf(plural));
587             }
588             else if (durationString.contains(singular))
589             {
590                 return durationString.substring(durationString.indexOf(singular));
591             }
592             else
593             {
594                 throw new InvalidDurationException("Not a valid durationString string");
595             }
596         }
597         return durationString.substring(1);
598     }
599 
600 
601 
602     /**
603      * Get String representation of a duration
604      * <p/>
605      *
606      * @param seconds Number of seconds
607      * @return String representing duration, eg: "1h 30m"
608      * @see #getDurationStringWithNegative(long)
609      */
610     public static String getDurationString(long seconds)
611     {
612         return getDurationStringSeconds(seconds, Duration.DAY.getSeconds(), Duration.WEEK.getSeconds());
613     }
614 
615     /**
616      * Get String representation of a (possibly negative) duration.
617      * <p/>
618      *
619      * @param seconds Number of seconds
620      * @return String representing duration, eg: "-1h 30m"
621      * @see #getDurationString(long)
622      */
623     public static String getDurationStringWithNegative(long seconds)
624     {
625         if (seconds < 0)
626         {
627             return "-" + getDurationString(-seconds);
628         }
629         else
630         {
631             return getDurationString(seconds);
632         }
633     }
634 
635     /**
636      * Get a duration string representing the given number of seconds. The string will use the largest unit possible.
637      * (i.e. 1w 3d)
638      * @param l the number of seconds
639      * @param hoursPerDay hours in a working day
640      * @param daysPerWeek days in a working week
641      * @return the duration string
642      */
643     public static String getDurationString(long l, int hoursPerDay, int daysPerWeek)
644     {
645         long secondsInDay = hoursPerDay * Duration.HOUR.getSeconds();
646         long secondsPerWeek = daysPerWeek * secondsInDay;
647         return getDurationStringSeconds(l, secondsInDay, secondsPerWeek);
648     }
649 
650     /**
651      * Get a duration string representing the given number of seconds. The string will use the largest unit possible.
652      * (i.e. 1w 3d).
653      * Use this method when you want to specify a non-integral number of hours in a day (e.g. 7.5) or days per week.
654      * @param l the number of seconds
655      * @param secondsPerDay the number of seconds in a working day
656      * @param secondsPerWeek the number of seconds in a working week
657      * @return the formatted duration string
658      */
659     public static String getDurationStringSeconds(long l, long secondsPerDay, long secondsPerWeek)
660     {
661         if (l == 0)
662         {
663             return "0m";
664         }
665 
666         StringBuilder result = new StringBuilder();
667 
668         if (l >= secondsPerWeek)
669         {
670             result.append((l / secondsPerWeek));
671             result.append("w ");
672             l = l % secondsPerWeek;
673         }
674 
675         if (l >= secondsPerDay)
676         {
677             result.append((l / secondsPerDay));
678             result.append("d ");
679             l = l % secondsPerDay;
680         }
681 
682         if (l >= Duration.HOUR.getSeconds())
683         {
684             result.append((l / Duration.HOUR.getSeconds()));
685             result.append("h ");
686             l = l % Duration.HOUR.getSeconds();
687         }
688 
689         if (l >= Duration.MINUTE.getSeconds())
690         {
691             result.append((l / Duration.MINUTE.getSeconds()));
692             result.append("m ");
693         }
694 
695         return result.toString().trim();
696     }
697 
698     /**
699      * Converts a number of seconds into a pretty formatted data string.  The resolution is in minutes.  So if the number of seconds is greater than a minute, it will
700      * only be shown down top minute resolution.  If the number of seconds is less than a minute it will be shown in seconds.
701      * <p/>
702      * So for example <code>76</code> becomes <code>'1 minute'</code>, while <code>42</code> becomes <code>'42 seconds'</code>
703      *
704      * @param numSecs        the number of seconds in the duration
705      * @param resourceBundle a resouce bundle for i18n
706      * @return a string in readable pretty duration format, using minute resolution
707      */
708     public static String getDurationPretty(long numSecs, ResourceBundle resourceBundle)
709     {
710         return getDurationPrettySeconds(numSecs, Duration.DAY.getSeconds(), Duration.WEEK.getSeconds(), resourceBundle, false);
711     }
712 
713     /**
714      * Converts a number of seconds into a pretty formatted data string.  The resolution is in minutes.  So if the number of seconds is greater than a minute, it will
715      * only be shown down top minute resolution.  If the number of seconds is less than a minute it will be shown in seconds.
716      * <p/>
717      * So for example <code>76</code> becomes <code>'1 minute'</code>, while <code>42</code> becomes <code>'42 seconds'</code>
718      *
719      * @param numSecs        the number of seconds in the duration
720      * @param hoursPerDay    the hours in a day
721      * @param daysPerWeek    the number of days in a week
722      * @param resourceBundle a resouce bundle for i18n
723      * @return a string in readable pretty duration format, using minute resolution
724      */
725     public static String getDurationPretty(long numSecs, int hoursPerDay, int daysPerWeek, ResourceBundle resourceBundle)
726     {
727         long secondsInDay = hoursPerDay * Duration.HOUR.getSeconds();
728         long secondsPerWeek = daysPerWeek * secondsInDay;
729         return getDurationPrettySeconds(numSecs, secondsInDay, secondsPerWeek, resourceBundle, false);
730     }
731 
732     /**
733      * Converts a number of seconds into a pretty formatted data string.  The resolution is in seconds.
734      * <p/>
735      * So for example <code>76</code> becomes <code>'1 minute, 16 seconds'</code>, while <code>42</code> becomes <code>'42 seconds'</code>
736      *
737      * @param numSecs        the number of seconds in the duration
738      * @param resourceBundle a resouce bundle for i18n
739      * @return a string in readable pretty duration format, using second resolution
740      */
741     public static String getDurationPrettySecondsResolution(long numSecs, ResourceBundle resourceBundle)
742     {
743         return getDurationPrettySeconds(numSecs, Duration.DAY.getSeconds(), Duration.WEEK.getSeconds(), resourceBundle, true);
744     }
745 
746     /**
747      * Converts a number of seconds into a pretty formatted data string.  The resolution is in seconds.
748      * <p/>
749      * So for example <code>76</code> becomes <code>'1 minute, 16 seconds'</code>, while <code>42</code> becomes <code>'42 seconds'</code>
750      *
751      * @param numSecs        the number of seconds in the duration
752      * @param hoursPerDay    the hours in a day
753      * @param daysPerWeek    the number of days in a week
754      * @param resourceBundle a resouce bundle for i18n
755      * @return a string in readable pretty duration format, using second resolution
756      */
757     public static String getDurationPrettySecondsResolution(long numSecs, int hoursPerDay, int daysPerWeek, ResourceBundle resourceBundle)
758     {
759         long secondsInDay = hoursPerDay * Duration.HOUR.getSeconds();
760         long secondsPerWeek = daysPerWeek * secondsInDay;
761         return getDurationPrettySeconds(numSecs, secondsInDay, secondsPerWeek, resourceBundle, true);
762     }
763 
764     /**
765      * Get a pretty formatted duration for the given number of seconds. (e.g. "4 days, 2 hours, 30 minutes")
766      * @param numSecs the number of seconds in the duration
767      * @param secondsPerDay the number of seconds in a "day"
768      * @param secondsPerWeek the number of seconds in a "week"
769      * @param resourceBundle the bundle containing translations for the strings used in the pretty string (e.g. "days")
770      * @param secondsDuration if false only display down to the minute even if there are some seconds, else display seconds
771      * @return the formatted pretty duration
772      */
773     private static String getDurationPrettySeconds(long numSecs, long secondsPerDay, long secondsPerWeek, ResourceBundle resourceBundle, boolean secondsDuration)
774     {
775         // use perWeek to calculate perYear because that already has "days per week" already figured in. if a week only had 3 days, for instance then
776         // doing secondsPerDay * 365 would overestimate how much we can get done in a year.
777         final long secondsPerYear = secondsPerWeek * 52;
778         return getDurationPrettySeconds(numSecs, secondsPerYear, secondsPerDay, secondsPerWeek, resourceBundle, secondsDuration);
779     }
780 
781     /**
782      * Get a pretty formatted duration for the given number of seconds. (e.g. "4 days, 2 hours, 30 minutes")
783      * @param numSecs the number of seconds in the duration
784      * @param secondsPerDay the number of seconds in a "day"
785      * @param secondsPerWeek the number of seconds in a "week"
786      * @param resourceBundle the bundle containing translations for the strings used in the pretty string (e.g. "days")
787      * @return the formatted pretty duration
788      */
789     public static String getDurationPrettySeconds(long numSecs, long secondsPerDay, long secondsPerWeek, ResourceBundle resourceBundle)
790     {
791         return getDurationPrettySeconds(numSecs, secondsPerDay, secondsPerWeek, resourceBundle, false);
792     }
793 
794     /*
795      * This implementation method returns things in "minute resolution" unless the secondResolution flag is true
796      */
797     private static String getDurationPrettySeconds(long numSecs, long secondsPerYear, long secondsPerDay, long secondsPerWeek, ResourceBundle resourceBundle, boolean secondResolution)
798     {
799         if (numSecs == 0)
800         {
801             if (secondResolution)
802             {
803                 return "0 " + getText(resourceBundle, "core.dateutils.seconds");
804             }
805             else
806             {
807                 return "0 " + getText(resourceBundle, "core.dateutils.minutes");
808             }
809         }
810 
811         StringBuilder result = new StringBuilder();
812 
813         if (numSecs >= secondsPerYear)
814         {
815             long years = numSecs / secondsPerYear;
816             result.append(years).append(' ');
817 
818             if (years > 1)
819             {
820                 result.append(getText(resourceBundle, "core.dateutils.years"));
821             }
822             else
823             {
824                 result.append(getText(resourceBundle, "core.dateutils.year"));
825             }
826 
827             result.append(", ");
828             numSecs = numSecs % secondsPerYear;
829         }
830 
831         if (numSecs >= secondsPerWeek)
832         {
833             long weeks = numSecs / secondsPerWeek;
834             result.append(weeks).append(' ');
835 
836             if (weeks > 1)
837             {
838                 result.append(getText(resourceBundle, "core.dateutils.weeks"));
839             }
840             else
841             {
842                 result.append(getText(resourceBundle, "core.dateutils.week"));
843             }
844 
845             result.append(", ");
846             numSecs = numSecs % secondsPerWeek;
847         }
848 
849         if (numSecs >= secondsPerDay)
850         {
851             long days = numSecs / secondsPerDay;
852             result.append(days).append(' ');
853 
854             if (days > 1)
855             {
856                 result.append(getText(resourceBundle, "core.dateutils.days"));
857             }
858             else
859             {
860                 result.append(getText(resourceBundle, "core.dateutils.day"));
861             }
862 
863             result.append(", ");
864             numSecs = numSecs % secondsPerDay;
865         }
866 
867         if (numSecs >= Duration.HOUR.getSeconds())
868         {
869             long hours = numSecs / Duration.HOUR.getSeconds();
870             result.append(hours).append(' ');
871 
872             if (hours > 1)
873             {
874                 result.append(getText(resourceBundle, "core.dateutils.hours"));
875             }
876             else
877             {
878                 result.append(getText(resourceBundle, "core.dateutils.hour"));
879             }
880 
881             result.append(", ");
882             numSecs = numSecs % Duration.HOUR.getSeconds();
883         }
884 
885         if (numSecs >= Duration.MINUTE.getSeconds())
886         {
887             long minute = numSecs / Duration.MINUTE.getSeconds();
888             result.append(minute).append(' ');
889 
890 
891             if (minute > 1)
892             {
893                 result.append(getText(resourceBundle, "core.dateutils.minutes"));
894             }
895             else
896             {
897                 result.append(getText(resourceBundle, "core.dateutils.minute"));
898             }
899 
900             result.append(", ");
901 
902             // if we want seconds resolution we need to reduce it down to seconds here
903             if (secondResolution)
904             {
905                 numSecs = numSecs % Duration.MINUTE.getSeconds();
906             }
907         }
908 
909         if (numSecs >= 1 && numSecs < Duration.MINUTE.getSeconds())
910         {
911             result.append(numSecs).append(' ');
912 
913 
914             if (numSecs > 1)
915             {
916                 result.append(getText(resourceBundle, "core.dateutils.seconds"));
917             }
918             else
919             {
920                 result.append(getText(resourceBundle, "core.dateutils.second"));
921             }
922 
923             result.append(", ");
924         }
925 
926         if (result.length() > 2) // remove the ", " on th end
927         {
928             return result.substring(0, result.length() - 2);
929         }
930         else
931         {
932             return result.toString();
933         }
934     }
935 
936     /** This is used by the Velocity templates as a bean
937      * @param l a duration in seconds
938      * @return a pretty formatted version of the duration
939      */
940     public String formatDurationPretty(long l)
941     {
942         return DateUtils.getDurationPretty(l, resourceBundle);
943     }
944 
945     /** This is used by the Velocity templates as a bean
946      * @param seconds duration as a string
947      * @return a pretty formatted version of the duration
948      */
949     public String formatDurationPretty(String seconds)
950     {
951         return DateUtils.getDurationPretty(Long.parseLong(seconds), resourceBundle);
952     }
953 
954 
955     /** This is used by the WebWork tags as a bean
956      * Despite the name it doesn't actually format a duration string. It takes a long.
957      * @param l a duration in seconds
958      * @return a pretty formatted version of the duration
959      * @deprecated You should be calling formatDurationPretty
960      * @see #formatDurationPretty
961      */
962     public String formatDurationString(long l)
963     {
964         return DateUtils.getDurationPretty(l, resourceBundle);
965     }
966 
967     private static String getText(ResourceBundle resourceBundle, String key)
968     {
969         try
970         {
971             return resourceBundle.getString(key);
972         }
973         catch (MissingResourceException e)
974         {
975             log.error(e);
976             return "";
977         }
978     }
979 
980     /**
981      * Change the date of a Calendar object so that it has the maximum resolution
982      * of "period" where period is one of the constants in CALENDAR_PERIODS above.
983      * <p/>
984      * e.g. to obtain the maximum value for a month, call toEndOfPeriod(calendarObject, Calendar.MONTH)
985      *
986      * @param calendar The Calendar to change
987      * @param period   The period to "maximise"
988      * @return A modified Calendar object
989      */
990     public static Calendar toEndOfPeriod(Calendar calendar, int period)
991     {
992         boolean zero = false;
993 
994         for (int calendarPeriod : CALENDAR_PERIODS)
995         {
996             if (zero)
997             {
998                 calendar.set(calendarPeriod, calendar.getMaximum(calendarPeriod));
999             }
1000 
1001             if (calendarPeriod == period)
1002             {
1003                 zero = true;
1004             }
1005         }
1006 
1007         if (!zero)
1008         {
1009             throw new IllegalArgumentException("unknown Calendar period: " + period);
1010         }
1011 
1012         return calendar;
1013     }
1014 
1015     /**
1016      * Change the date of a Calendar object so that it has the minimum resolution
1017      * of "period" where period is one of the constants in CALENDAR_PERIODS above.
1018      * @param calendar calendar to modify
1019      * @param period the new calendar period from CALENDAR_PERIODS
1020      * @return the calendar that was passed in
1021      */
1022     public static Calendar toStartOfPeriod(Calendar calendar, int period)
1023     {
1024         boolean zero = false;
1025         for (int calendarPeriod : CALENDAR_PERIODS)
1026         {
1027             if (zero)
1028             {
1029                 if (calendarPeriod == Calendar.DAY_OF_MONTH)
1030                 {
1031                     calendar.set(Calendar.DAY_OF_MONTH, 1);
1032                 }
1033                 else
1034                 {
1035                     calendar.set(calendarPeriod, 0);
1036                 }
1037             }
1038 
1039             if (calendarPeriod == period)
1040             {
1041                 zero = true;
1042             }
1043 
1044         }
1045 
1046         if (!zero)
1047         {
1048             throw new IllegalArgumentException("unknown Calendar period: " + period);
1049         }
1050 
1051         return calendar;
1052     }
1053 
1054     /**
1055      * Given a period, and a date that falls within that period, create a range of dates such
1056      * that the period is contained exactly within [startDate <= {range} < endDate]
1057      *
1058      * @param date   a calendar object of a date falling in that range
1059      * @param period something in CALENDAR_PERIODS
1060      * @return resulting range of dates
1061      */
1062     public static DateRange toDateRange(Calendar date, int period)
1063     {
1064         // defensively copy the calendar so we don't break anything outside.
1065         Calendar cal = (Calendar) date.clone();
1066         toStartOfPeriod(cal, period);
1067         Date startDate = new Date(cal.getTimeInMillis());
1068         cal.add(period, 1);
1069         Date endDate = new Date(cal.getTimeInMillis());
1070 
1071         return new DateRange(startDate, endDate);
1072     }
1073 
1074     public static Calendar getCalendarDay(int year, int month, int day)
1075     {
1076         return initCalendar(year, month, day, 0, 0, 0, 0);
1077     }
1078 
1079     public static Date getDateDay(int year, int month, int day)
1080     {
1081         return getCalendarDay(year, month, day).getTime();
1082     }
1083 
1084     public static Date getSqlDateDay(int year, int month, int day)
1085     {
1086         return new java.sql.Date(getCalendarDay(year, month, day).getTimeInMillis());
1087     }
1088 
1089     public static int get24HourTime(final String meridianIndicator, final int hours)
1090     {
1091         // two special cases 12 AM & 12 PM
1092         if (hours == 12)
1093         {
1094             if (AM.equalsIgnoreCase(meridianIndicator))
1095             {
1096                 return 0;
1097             }
1098 
1099             if (PM.equalsIgnoreCase(meridianIndicator))
1100             {
1101                 return 12;
1102             }
1103         }
1104 
1105         final int onceMeridianAdjustment = PM.equalsIgnoreCase(meridianIndicator) ? 12 : 0;
1106         return hours + onceMeridianAdjustment;
1107     }
1108 
1109 
1110     public static Date tomorrow()
1111     {
1112         Calendar cal = Calendar.getInstance();
1113         cal.add(Calendar.DAY_OF_MONTH, 1);
1114         return cal.getTime();
1115     }
1116 
1117     public static Date yesterday()
1118     {
1119         Calendar cal = Calendar.getInstance();
1120         cal.add(Calendar.DAY_OF_MONTH, -1);
1121         return cal.getTime();
1122     }
1123 
1124     private static Calendar initCalendar(int year, int month, int day, int hour, int minute, int second, int millis)
1125     {
1126         Calendar calendar = Calendar.getInstance();
1127         calendar.set(year, month, day, hour, minute, second);
1128         calendar.set(Calendar.MILLISECOND, millis);
1129         return calendar;
1130     }
1131 }