1   package com.atlassian.core.util;
2   
3   import java.util.Collections;
4   import java.util.HashMap;
5   import java.util.Locale;
6   import java.util.Map;
7   import java.util.MissingResourceException;
8   
9   import com.atlassian.core.i18n.I18nTextProvider;
10  import com.atlassian.core.util.DateUtils.Duration;
11  
12  import org.junit.Test;
13  
14  import static org.junit.Assert.assertEquals;
15  import static org.junit.Assert.assertFalse;
16  
17  public class TestDurationUtils
18  {
19      private static final long SECONDS_IN_DAY = 60 * 60 * 24;
20      private static final long SECONDS_IN_WEEK = SECONDS_IN_DAY * 7;
21      
22      static Map<String, Duration> defaultDurationTokens()
23      {
24          Map<String, Duration> tokens = new HashMap<String, Duration>();
25          tokens.put("d", Duration.DAY);
26          tokens.put("h", Duration.HOUR);
27          tokens.put("m", Duration.MINUTE);
28          return tokens;
29      }
30      
31      static Map<String, Duration> durationTokensForPrettyFormat()
32      {
33          Map<String, Duration> tokens = new HashMap<String, Duration>();
34          tokens.put("day", Duration.DAY);
35          tokens.put("hour", Duration.HOUR);
36          tokens.put("minute", Duration.MINUTE);
37          tokens.put("week", Duration.WEEK);
38          tokens.put("year", Duration.YEAR);
39          tokens.put("days", Duration.DAY);
40          tokens.put("hours", Duration.HOUR);
41          tokens.put("minutes", Duration.MINUTE);
42          tokens.put("weeks", Duration.WEEK);
43          tokens.put("years", Duration.YEAR);
44          return tokens;
45      }
46      
47      static long toSeconds(String s, Duration defaultUnit) throws InvalidDurationException
48      {
49          return DurationUtils.getDurationSeconds(s, SECONDS_IN_DAY, SECONDS_IN_WEEK, defaultUnit, Locale.ENGLISH, defaultDurationTokens());
50      }
51      
52      static long toSeconds(String s) throws InvalidDurationException
53      {
54          return toSeconds(s, Locale.ENGLISH, defaultDurationTokens());
55      }
56      
57      static long toSeconds(String s, Map<String, Duration> durationTokens) throws InvalidDurationException
58      {
59          return toSeconds(s, Locale.ENGLISH, durationTokens);
60      }
61      
62      static long toSeconds(String s, Locale locale, Map<String, Duration> durationTokens) throws InvalidDurationException
63      {
64          return DurationUtils.getDurationSeconds(s, SECONDS_IN_DAY, SECONDS_IN_WEEK, null, locale, durationTokens);
65      }
66      
67      static boolean validDuration(String s, Locale locale, Map<String, Duration> durationTokens)
68      {
69          try
70          {
71              DurationUtils.getDurationSeconds(s, SECONDS_IN_DAY, SECONDS_IN_WEEK, null, locale, durationTokens);
72              return true;
73          }
74          catch (InvalidDurationException ide)
75          {
76              return false;
77          }
78      }
79      
80      @Test
81      public void blankStringIsEmptyDuration() throws InvalidDurationException
82      {
83          assertEquals(0, toSeconds(null));
84          assertEquals(0, toSeconds(""));
85          assertEquals(0, toSeconds(" "));
86      }
87      
88      @Test
89      public void parseJustNumberUsingDefaultUnit() throws InvalidDurationException
90      {
91          assertEquals(1, toSeconds("1", Duration.SECOND));
92          assertEquals(60, toSeconds("1", Duration.MINUTE));
93      }
94      
95      @Test(expected = InvalidDurationException.class)
96      public void badNumbersCauseException() throws InvalidDurationException
97      {
98          toSeconds("1.0.5", Duration.SECOND);
99      }
100     
101     @Test
102     public void parseJustNumberUsingDefaultUnitWithLeadingTrailingSpaces() throws InvalidDurationException
103     {
104         assertEquals(1, toSeconds(" 1", Duration.SECOND));
105         assertEquals(1, toSeconds("1 ", Duration.SECOND));
106         assertEquals(1, toSeconds(" 1 ", Duration.SECOND));
107     }
108     
109     @Test
110     public void moreThanOneNumberWithOneUsingDefaultUnits() throws InvalidDurationException
111     {
112         assertEquals(90, toSeconds("1m 30", Duration.SECOND));
113     }
114     
115     @Test
116     public void moreThanOneNumberWithOneUsingDefaultUnitsWithLeadingTrailingSpaces() throws InvalidDurationException
117     {
118         assertEquals(90, toSeconds(" 1m 30", Duration.SECOND));
119         assertEquals(90, toSeconds("1m 30 ", Duration.SECOND));
120         assertEquals(90, toSeconds(" 1m 30 ", Duration.SECOND));
121     }
122     
123     @Test(expected = InvalidDurationException.class)
124     public void secondsNotSupportedAsUnit() throws InvalidDurationException
125     {
126         toSeconds("30s");
127     }
128     
129     @Test
130     public void moreThanOneNumberWithUnitsSpecified() throws InvalidDurationException
131     {
132         assertEquals(5400, toSeconds("1h 30m"));
133         assertEquals(5400, toSeconds("30m 1h"));
134         assertEquals(129600, toSeconds("12h 1d"));
135         assertEquals(129600, toSeconds("1d 12h"));
136     }
137 
138     @Test
139     public void fractionalUnitsPermitted() throws InvalidDurationException
140     {
141         assertEquals(1800, toSeconds("0.5h"));
142     }
143     
144     @Test(expected = InvalidDurationException.class)
145     public void durationsMustRoundToWholeMinutes() throws InvalidDurationException
146     {
147         toSeconds("0.5m");
148     }
149     
150     @Test
151     public void durationUsingDifferentUnits() throws InvalidDurationException
152     {
153         assertEquals(86400, toSeconds("1t", Collections.singletonMap("t", Duration.DAY)));
154     }
155     
156     @Test
157     public void durationUsingMultipleDifferentUnits() throws InvalidDurationException
158     {
159         Map<String, Duration> durations = new HashMap<String, Duration>();
160         durations.put("t", Duration.DAY);
161         durations.put("h", Duration.HOUR);
162         
163         assertEquals(129600, toSeconds("12h 1t", durations));
164     }
165     
166     @Test
167     public void durationUsingDifferentDecimalFormat() throws InvalidDurationException
168     {
169         assertEquals(5400, toSeconds("1,5h", Locale.GERMANY, Collections.singletonMap("h", Duration.HOUR)));
170     }
171     
172     @Test
173     public void durationUsingDifferentDigits() throws InvalidDurationException
174     {
175         // Indian numerals: 1234.5
176         assertEquals(4444200, toSeconds("\u0967,\u0968\u0969\u096A.\u096Bh", new Locale("hi", "IN"), defaultDurationTokens()));
177     }
178     
179     @Test
180     public void durationUsingNonAsciiUnits() throws InvalidDurationException
181     {
182         Map<String, Duration> durations = new HashMap<String, Duration>();
183         durations.put("\u65e5", Duration.DAY);
184         durations.put("\u6642\u9593", Duration.HOUR);
185         durations.put("\u5206", Duration.MINUTE);
186         
187         assertEquals(352860, toSeconds("1 \u5206 2 \u6642\u9593 4 \u65e5", durations));
188     }
189     
190     @Test
191     public void durationTokensFromI18nResources()
192     {
193         Map<String, String> props = new HashMap<String, String>();
194         props.put("core.durationutils.unit.hour", "{0}h");
195         props.put("core.durationutils.unit.day", "{0}d");
196         props.put("core.durationutils.unit.minute", "{0}m");
197         props.put("core.dateutils.second", "second");
198         props.put("core.dateutils.seconds", "seconds");
199         props.put("core.dateutils.minute", "minute");
200         props.put("core.dateutils.minutes", "minutes");
201         props.put("core.dateutils.hour", "hour");
202         props.put("core.dateutils.hours", "hours");
203         props.put("core.dateutils.day", "day");
204         props.put("core.dateutils.days", "days");
205         props.put("core.dateutils.week", "week");
206         props.put("core.dateutils.weeks", "weeks");
207         props.put("core.dateutils.month", "month");
208         props.put("core.dateutils.months", "months");
209         props.put("core.dateutils.year", "year");
210         props.put("core.dateutils.years", "years");
211         
212         Map<String, Duration> expectedDurations = new HashMap<String, Duration>();
213         expectedDurations.put("d", Duration.DAY);
214         expectedDurations.put("h", Duration.HOUR);
215         expectedDurations.put("m", Duration.MINUTE);
216         expectedDurations.put("second", Duration.SECOND);
217         expectedDurations.put("seconds", Duration.SECOND);
218         expectedDurations.put("day", Duration.DAY);
219         expectedDurations.put("hour", Duration.HOUR);
220         expectedDurations.put("minute", Duration.MINUTE);
221         expectedDurations.put("week", Duration.WEEK);
222         expectedDurations.put("month", Duration.MONTH);
223         expectedDurations.put("year", Duration.YEAR);
224         expectedDurations.put("days", Duration.DAY);
225         expectedDurations.put("hours", Duration.HOUR);
226         expectedDurations.put("minutes", Duration.MINUTE);
227         expectedDurations.put("weeks", Duration.WEEK);
228         expectedDurations.put("months", Duration.MONTH);
229         expectedDurations.put("years", Duration.YEAR);
230         
231         Map<String, Duration> durations = DurationUtils.getDurationTokens(i18nForProps(props));
232         
233         assertEquals(expectedDurations, durations);
234     }
235     
236     @Test
237     public void durationTokensFromI18nResourcesNonAsciiUnits()
238     {
239         Map<String, String> props = new HashMap<String, String>();
240         props.put("core.durationutils.unit.hour", "{0} \u6642\u9593");
241         props.put("core.durationutils.unit.day", "{0} \u65e5");
242         props.put("core.durationutils.unit.minute", "{0} \u5206");
243         props.put("core.dateutils.second", "\u79d2");
244         props.put("core.dateutils.seconds", "\u79d2");
245         props.put("core.dateutils.minute", "\u5206");
246         props.put("core.dateutils.minutes", "\u5206");
247         props.put("core.dateutils.hour", "\u6642\u9593");
248         props.put("core.dateutils.hours", "\u6642\u9593");
249         props.put("core.dateutils.day", "\u65e5");
250         props.put("core.dateutils.days", "\u65e5");
251         props.put("core.dateutils.week", "\u9031");
252         props.put("core.dateutils.weeks", "\u9031");
253         props.put("core.dateutils.month", "\u6708");
254         props.put("core.dateutils.months", "\u6708");
255         props.put("core.dateutils.year", "\u5e74");
256         props.put("core.dateutils.years", "\u5e74");
257 
258         Map<String, Duration> expectedDurations = new HashMap<String, Duration>();
259         expectedDurations.put("\u79d2", Duration.SECOND);
260         expectedDurations.put("\u65e5", Duration.DAY);
261         expectedDurations.put("\u6642\u9593", Duration.HOUR);
262         expectedDurations.put("\u5206", Duration.MINUTE);
263         expectedDurations.put("\u9031", Duration.WEEK);
264         expectedDurations.put("\u6708", Duration.MONTH);
265         expectedDurations.put("\u5e74", Duration.YEAR);
266         
267         Map<String, Duration> durations = DurationUtils.getDurationTokens(i18nForProps(props));
268         
269         assertEquals(expectedDurations, durations);
270     }
271     
272     @Test(expected = MissingResourceException.class)
273     public void durationTokensFailsWhenResourceMissing()
274     {
275         DurationUtils.getDurationTokens(i18nForProps(Collections.<String, String>emptyMap()));
276     }
277     
278     @Test
279     public void acceptPrettyFormatDurations() throws InvalidDurationException
280     {
281         assertEquals(0, toSeconds("0 minutes", durationTokensForPrettyFormat()));
282         assertEquals(600, toSeconds("10 minutes", durationTokensForPrettyFormat()));
283         assertEquals(3660, toSeconds("1 hour, 1 minute", durationTokensForPrettyFormat()));
284         assertEquals(90060, toSeconds("1 day, 1 hour, 1 minute", durationTokensForPrettyFormat()));
285         assertEquals(694860, toSeconds("1 week, 1 day, 1 hour, 1 minute", durationTokensForPrettyFormat()));
286     }
287     
288     @Test
289     public void ensureBadStringsFailToParse()
290     {
291         assertFalse(validDuration("h", Locale.ENGLISH, defaultDurationTokens()));
292         assertFalse(validDuration("0x", Locale.ENGLISH, defaultDurationTokens()));
293     }
294     
295     static I18nTextProvider i18nForProps(final Map<String, String> props)
296     {
297         return new I18nTextProvider()
298         {
299             public String getText(String key) throws MissingResourceException
300             {
301                 if (props.containsKey(key))
302                 {
303                     return props.get(key);
304                 }
305                 else
306                 {
307                     throw new MissingResourceException("No 18n string available for '" + key + "'", getClass().getName(), key);
308                 }
309             }
310             
311             public String getText(String key, Object[] args)
312             {
313                 throw new UnsupportedOperationException("Test stub");
314             }
315         };
316     }
317 }