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
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 }