View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.lang3.time;
19  
20  import static org.apache.commons.lang3.LangAssertions.assertIllegalArgumentException;
21  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
22  import static org.junit.jupiter.api.Assertions.assertEquals;
23  import static org.junit.jupiter.api.Assertions.assertFalse;
24  import static org.junit.jupiter.api.Assertions.assertNotEquals;
25  import static org.junit.jupiter.api.Assertions.assertNotNull;
26  import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
27  import static org.junit.jupiter.api.Assertions.assertTrue;
28  
29  import java.lang.reflect.Constructor;
30  import java.lang.reflect.Modifier;
31  import java.time.Duration;
32  import java.util.Calendar;
33  import java.util.TimeZone;
34  
35  import org.apache.commons.lang3.AbstractLangTest;
36  import org.apache.commons.lang3.time.DurationFormatUtils.Token;
37  import org.junit.jupiter.api.Test;
38  import org.junit.jupiter.params.ParameterizedTest;
39  import org.junit.jupiter.params.provider.Arguments;
40  import org.junit.jupiter.params.provider.MethodSource;
41  import org.junitpioneer.jupiter.DefaultTimeZone;
42  
43  /**
44   * Tests {@link DurationFormatUtils}.
45   * <p>
46   * NOT THREAD-SAFE.
47   * </p>
48   */
49  class DurationFormatUtilsTest extends AbstractLangTest {
50  
51      private static final int FOUR_YEARS = 365 * 3 + 366;
52  
53      private static Arguments[] testLANG1827() {
54          final long twoHours = Duration.ofHours(2).toMillis();
55          final long twoHoursThirtyMin = Duration.ofHours(2).plusMinutes(30).toMillis();
56          final long oneDayTwoHours = Duration.ofDays(1).plusHours(2).toMillis();
57          return new Arguments[] {
58                  Arguments.of("escaped quote inside literal", "2 o'clock", twoHours, "H' o''clock'"),
59                  Arguments.of("escaped quote at start of literal", "it's 2 hours", twoHours, "'it''s 'H' hours'"),
60                  Arguments.of("escaped quote outside literal", "2'30", twoHoursThirtyMin, "H''m"),
61                  Arguments.of("multiple escaped quotes", "it's been 1 day's and 2 hour's", oneDayTwoHours,
62                          "'it''s been 'd' day''s and 'H' hour''s'"),
63                  Arguments.of("standalone escaped quote", "2h'30m", twoHoursThirtyMin, "H'h'''m'm'"),
64                  Arguments.of("escaped quote inside optional block", "2 hour's", twoHours, "[d' day''s ']H' hour''s'"),
65                  Arguments.of("existing literal behavior", "2 hours 30 minutes", twoHoursThirtyMin, "H' hours 'm' minutes'")
66          };
67      }
68  
69      private void assertEqualDuration(final String expected, final int[] start, final int[] end, final String format) {
70          assertEqualDuration(null, expected, start, end, format);
71      }
72  
73      private void assertEqualDuration(final String message, final String expected, final int[] start, final int[] end, final String format) {
74          final Calendar cal1 = Calendar.getInstance();
75          cal1.set(start[0], start[1], start[2], start[3], start[4], start[5]);
76          cal1.set(Calendar.MILLISECOND, 0);
77          final Calendar cal2 = Calendar.getInstance();
78          cal2.set(end[0], end[1], end[2], end[3], end[4], end[5]);
79          cal2.set(Calendar.MILLISECOND, 0);
80          final long milli1 = cal1.getTime().getTime();
81          final long milli2 = cal2.getTime().getTime();
82          final String result = DurationFormatUtils.formatPeriod(milli1, milli2, format);
83          if (message == null) {
84              assertEquals(expected, result);
85          } else {
86              assertEquals(expected, result, message);
87          }
88      }
89  
90      private void assertFormatPeriodOneMilli(final long startMillis, final long endMillis) {
91          final TimeZone gmt = TimeZones.getTimeZone("GMT");
92          assertEquals("1", DurationFormatUtils.formatPeriod(startMillis, endMillis, "S", true, gmt));
93          assertEquals("0/0/0/0/0/0.001",
94                  DurationFormatUtils.formatPeriod(startMillis, endMillis, "y/M/d/H/m/s.S", true, gmt));
95          // formatPeriod must agree with formatDuration over the full range, not just spans near the epoch.
96          assertEquals(DurationFormatUtils.formatDuration(endMillis - startMillis, "S"),
97                  DurationFormatUtils.formatPeriod(startMillis, endMillis, "S", true, gmt));
98      }
99  
100     private void bruteForce(final int year, final int month, final int day, final String format, final int calendarType) {
101         final String msg = year + "-" + month + "-" + day + " to ";
102         final Calendar c = Calendar.getInstance();
103         c.set(year, month, day, 0, 0, 0);
104         final int[] array1 = { year, month, day, 0, 0, 0 };
105         final int[] array2 = { year, month, day, 0, 0, 0 };
106         for (int i = 0; i < FOUR_YEARS; i++) {
107             array2[0] = c.get(Calendar.YEAR);
108             array2[1] = c.get(Calendar.MONTH);
109             array2[2] = c.get(Calendar.DAY_OF_MONTH);
110             final String tmpMsg = msg + array2[0] + "-" + array2[1] + "-" + array2[2] + " at ";
111             assertEqualDuration(tmpMsg + i, Integer.toString(i), array1, array2, format);
112             c.add(calendarType, 1);
113         }
114     }
115 
116     private DurationFormatUtils.Token createTokenWithCount(final CharSequence value, final int count) {
117         final DurationFormatUtils.Token token = new DurationFormatUtils.Token(value, false, -1);
118         // To help debugging, toString() on a Token should never blow up.
119         assertNotNull(token.toString());
120         for (int i = 1; i < count; i++) {
121             token.increment();
122             assertNotNull(token.toString());
123         }
124         return token;
125     }
126 
127     @Test
128     void testAlternatingLiteralOptionals() {
129         final String format = "['d'dH'h'][m'm']['s's]['ms'S]";
130         assertEquals("d1", DurationFormatUtils.formatDuration(Duration.ofDays(1).toMillis(), format));
131         assertEquals("1h", DurationFormatUtils.formatDuration(Duration.ofHours(1).toMillis(), format));
132         assertEquals("1m", DurationFormatUtils.formatDuration(Duration.ofMinutes(1).toMillis(), format));
133         assertEquals("s1", DurationFormatUtils.formatDuration(Duration.ofSeconds(1).toMillis(), format));
134         assertEquals("ms001", DurationFormatUtils.formatDuration(Duration.ofMillis(1).toMillis(), format));
135         assertEquals("d1s1", DurationFormatUtils.formatDuration(Duration.ofDays(1).plusSeconds(1).toMillis(), format));
136         assertEquals("d11h", DurationFormatUtils.formatDuration(Duration.ofDays(1).plusHours(1).toMillis(), format));
137         assertEquals("d11h1m", DurationFormatUtils.formatDuration(Duration.ofDays(1).plusHours(1).plusMinutes(1).toMillis(), format));
138         assertEquals("d11h1ms1", DurationFormatUtils.formatDuration(Duration.ofDays(1).plusHours(1).plusMinutes(1).plusSeconds(1).toMillis(), format));
139         assertEquals("d11h1ms1ms001",
140                 DurationFormatUtils.formatDuration(Duration.ofDays(1).plusHours(1).plusMinutes(1).plusSeconds(1).plusMillis(1).toMillis(), format));
141     }
142 
143     /** See https://issues.apache.org/bugzilla/show_bug.cgi?id=38401 */
144     @Test
145     void testBugzilla38401() {
146         assertEqualDuration("0000/00/30 16:00:00 000", new int[] { 2006, 0, 26, 18, 47, 34 },
147                              new int[] { 2006, 1, 26, 10, 47, 34 }, "yyyy/MM/dd HH:mm:ss SSS");
148     }
149 
150     @SuppressWarnings("deprecation")
151     @Test
152     void testConstructor() {
153         assertNotNull(new DurationFormatUtils());
154         final Constructor<?>[] cons = DurationFormatUtils.class.getDeclaredConstructors();
155         assertEquals(1, cons.length);
156         assertTrue(Modifier.isPublic(cons[0].getModifiers()));
157         assertTrue(Modifier.isPublic(DurationFormatUtils.class.getModifiers()));
158         assertFalse(Modifier.isFinal(DurationFormatUtils.class.getModifiers()));
159     }
160 
161     @Test
162     void testDurationsByBruteForce() {
163         bruteForce(2006, 0, 1, "d", Calendar.DAY_OF_MONTH);
164         bruteForce(2006, 0, 2, "d", Calendar.DAY_OF_MONTH);
165         bruteForce(2007, 1, 2, "d", Calendar.DAY_OF_MONTH);
166         bruteForce(2004, 1, 29, "d", Calendar.DAY_OF_MONTH);
167         bruteForce(1996, 1, 29, "d", Calendar.DAY_OF_MONTH);
168 
169         bruteForce(1969, 1, 28, "M", Calendar.MONTH);  // tests for 48 years
170         //bruteForce(1996, 1, 29, "M", Calendar.MONTH);  // this will fail
171     }
172 
173     /** Attempting to test edge cases in DurationFormatUtils.formatPeriod. */
174     @Test
175     @DefaultTimeZone(TimeZones.GMT_ID)
176     void testEdgeDurations() {
177         // This test case must use a time zone without DST
178         TimeZone.setDefault(FastTimeZone.getGmtTimeZone());
179         assertEqualDuration("01", new int[] { 2006, 0, 15, 0, 0, 0 }, new int[] { 2006, 2, 10, 0, 0, 0 }, "MM");
180         assertEqualDuration("12", new int[] { 2005, 0, 15, 0, 0, 0 }, new int[] { 2006, 0, 15, 0, 0, 0 }, "MM");
181         assertEqualDuration("12", new int[] { 2005, 0, 15, 0, 0, 0 }, new int[] { 2006, 0, 16, 0, 0, 0 }, "MM");
182         assertEqualDuration("11", new int[] { 2005, 0, 15, 0, 0, 0 }, new int[] { 2006, 0, 14, 0, 0, 0 }, "MM");
183         assertEqualDuration("01 26", new int[] { 2006, 0, 15, 0, 0, 0 }, new int[] { 2006, 2, 10, 0, 0, 0 }, "MM dd");
184         assertEqualDuration("54", new int[] { 2006, 0, 15, 0, 0, 0 }, new int[] { 2006, 2, 10, 0, 0, 0 }, "dd");
185         assertEqualDuration("09 12", new int[] { 2006, 1, 20, 0, 0, 0 }, new int[] { 2006, 11, 4, 0, 0, 0 }, "MM dd");
186         assertEqualDuration("287", new int[] { 2006, 1, 20, 0, 0, 0 }, new int[] { 2006, 11, 4, 0, 0, 0 }, "dd");
187         assertEqualDuration("11 30", new int[] { 2006, 0, 2, 0, 0, 0 }, new int[] { 2007, 0, 1, 0, 0, 0 }, "MM dd");
188         assertEqualDuration("364", new int[] { 2006, 0, 2, 0, 0, 0 }, new int[] { 2007, 0, 1, 0, 0, 0 }, "dd");
189         assertEqualDuration("12 00", new int[] { 2006, 0, 1, 0, 0, 0 }, new int[] { 2007, 0, 1, 0, 0, 0 }, "MM dd");
190         assertEqualDuration("365", new int[] { 2006, 0, 1, 0, 0, 0 }, new int[] { 2007, 0, 1, 0, 0, 0 }, "dd");
191         assertEqualDuration("31", new int[] { 2006, 0, 1, 0, 0, 0 }, new int[] { 2006, 1, 1, 0, 0, 0 }, "dd");
192         assertEqualDuration("92", new int[] { 2005, 9, 1, 0, 0, 0 }, new int[] { 2006, 0, 1, 0, 0, 0 }, "dd");
193         assertEqualDuration("77", new int[] { 2005, 9, 16, 0, 0, 0 }, new int[] { 2006, 0, 1, 0, 0, 0 }, "dd");
194         // test month larger in start than end
195         assertEqualDuration("136", new int[] { 2005, 9, 16, 0, 0, 0 }, new int[] { 2006, 2, 1, 0, 0, 0 }, "dd");
196         // test when start in leap year
197         assertEqualDuration("136", new int[] { 2004, 9, 16, 0, 0, 0 }, new int[] { 2005, 2, 1, 0, 0, 0 }, "dd");
198         // test when end in leap year
199         assertEqualDuration("137", new int[] { 2003, 9, 16, 0, 0, 0 }, new int[] { 2004, 2, 1, 0, 0, 0 }, "dd");
200         // test when end in leap year but less than end of feb
201         assertEqualDuration("135", new int[] { 2003, 9, 16, 0, 0, 0 }, new int[] { 2004, 1, 28, 0, 0, 0 }, "dd");
202         assertEqualDuration("364", new int[] { 2007, 0, 2, 0, 0, 0 }, new int[] { 2008, 0, 1, 0, 0, 0 }, "dd");
203         assertEqualDuration("729", new int[] { 2006, 0, 2, 0, 0, 0 }, new int[] { 2008, 0, 1, 0, 0, 0 }, "dd");
204         assertEqualDuration("365", new int[] { 2007, 2, 2, 0, 0, 0 }, new int[] { 2008, 2, 1, 0, 0, 0 }, "dd");
205         assertEqualDuration("333", new int[] { 2007, 1, 2, 0, 0, 0 }, new int[] { 2008, 0, 1, 0, 0, 0 }, "dd");
206         assertEqualDuration("28", new int[] { 2008, 1, 2, 0, 0, 0 }, new int[] { 2008, 2, 1, 0, 0, 0 }, "dd");
207         assertEqualDuration("393", new int[] { 2007, 1, 2, 0, 0, 0 }, new int[] { 2008, 2, 1, 0, 0, 0 }, "dd");
208         assertEqualDuration("369", new int[] { 2004, 0, 29, 0, 0, 0 }, new int[] { 2005, 1, 1, 0, 0, 0 }, "dd");
209         assertEqualDuration("338", new int[] { 2004, 1, 29, 0, 0, 0 }, new int[] { 2005, 1, 1, 0, 0, 0 }, "dd");
210         assertEqualDuration("28", new int[] { 2004, 2, 8, 0, 0, 0 }, new int[] { 2004, 3, 5, 0, 0, 0 }, "dd");
211         assertEqualDuration("48", new int[] { 1992, 1, 29, 0, 0, 0 }, new int[] { 1996, 1, 29, 0, 0, 0 }, "M");
212         // this seems odd - and will fail if I throw it in as a brute force
213         // below as it expects the answer to be 12. It's a tricky edge case
214         assertEqualDuration("11", new int[] { 1996, 1, 29, 0, 0, 0 }, new int[] { 1997, 1, 28, 0, 0, 0 }, "M");
215         // again - this seems odd
216         assertEqualDuration("11 28", new int[] { 1996, 1, 29, 0, 0, 0 }, new int[] { 1997, 1, 28, 0, 0, 0 }, "M d");
217     }
218 
219     @Test
220     void testEmptyOptionals() {
221         assertEquals("", DurationFormatUtils.formatDuration(0L, "[d'd'][H'h'][m'm'][s's']"));
222         assertEquals("", DurationFormatUtils.formatDuration(0L, "['d''h''m''s's]"));
223     }
224 
225     @Test
226     void testFormatDuration() {
227         long duration = 0;
228         assertEquals("0", DurationFormatUtils.formatDuration(duration, "y"));
229         assertEquals("0", DurationFormatUtils.formatDuration(duration, "M"));
230         assertEquals("0", DurationFormatUtils.formatDuration(duration, "d"));
231         assertEquals("0", DurationFormatUtils.formatDuration(duration, "H"));
232         assertEquals("0", DurationFormatUtils.formatDuration(duration, "m"));
233         assertEquals("0", DurationFormatUtils.formatDuration(duration, "s"));
234         assertEquals("0", DurationFormatUtils.formatDuration(duration, "S"));
235         assertEquals("0000", DurationFormatUtils.formatDuration(duration, "SSSS"));
236         assertEquals("0000", DurationFormatUtils.formatDuration(duration, "yyyy"));
237         assertEquals("0000", DurationFormatUtils.formatDuration(duration, "yyMM"));
238 
239         duration = 60 * 1000;
240         assertEquals("0", DurationFormatUtils.formatDuration(duration, "y"));
241         assertEquals("0", DurationFormatUtils.formatDuration(duration, "M"));
242         assertEquals("0", DurationFormatUtils.formatDuration(duration, "d"));
243         assertEquals("0", DurationFormatUtils.formatDuration(duration, "H"));
244         assertEquals("1", DurationFormatUtils.formatDuration(duration, "m"));
245         assertEquals("60", DurationFormatUtils.formatDuration(duration, "s"));
246         assertEquals("60000", DurationFormatUtils.formatDuration(duration, "S"));
247         assertEquals("01:00", DurationFormatUtils.formatDuration(duration, "mm:ss"));
248 
249         final Calendar base = Calendar.getInstance();
250         base.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
251         base.set(Calendar.MILLISECOND, 0);
252 
253         final Calendar cal = Calendar.getInstance();
254         cal.set(2003, Calendar.FEBRUARY, 1, 0, 0, 0);
255         cal.set(Calendar.MILLISECOND, 0);
256         duration = cal.getTime().getTime() - base.getTime().getTime(); // duration from 2000-01-01 to cal
257         // don't use 1970 in test as time zones were less reliable in 1970 than now
258         // remember that duration formatting ignores time zones, working on strict hour lengths
259         final int days = 366 + 365 + 365 + 31;
260         assertEquals("0 0 " + days, DurationFormatUtils.formatDuration(duration, "y M d"));
261     }
262 
263     @Test
264     void testFormatDurationHMS() {
265         long time = 0;
266         assertEquals("00:00:00.000", DurationFormatUtils.formatDurationHMS(time));
267         time = 1;
268         assertEquals("00:00:00.001", DurationFormatUtils.formatDurationHMS(time));
269         time = 15;
270         assertEquals("00:00:00.015", DurationFormatUtils.formatDurationHMS(time));
271         time = 165;
272         assertEquals("00:00:00.165", DurationFormatUtils.formatDurationHMS(time));
273         time = 1675;
274         assertEquals("00:00:01.675", DurationFormatUtils.formatDurationHMS(time));
275         time = 13465;
276         assertEquals("00:00:13.465", DurationFormatUtils.formatDurationHMS(time));
277         time = 72789;
278         assertEquals("00:01:12.789", DurationFormatUtils.formatDurationHMS(time));
279         time = 12789 + 32 * 60000;
280         assertEquals("00:32:12.789", DurationFormatUtils.formatDurationHMS(time));
281         time = 12789 + 62 * 60000;
282         assertEquals("01:02:12.789", DurationFormatUtils.formatDurationHMS(time));
283     }
284 
285     @Test
286     void testFormatDurationISO() {
287         assertEquals("P0Y0M0DT0H0M0.000S", DurationFormatUtils.formatDurationISO(0L));
288         assertEquals("P0Y0M0DT0H0M0.001S", DurationFormatUtils.formatDurationISO(1L));
289         assertEquals("P0Y0M0DT0H0M0.010S", DurationFormatUtils.formatDurationISO(10L));
290         assertEquals("P0Y0M0DT0H0M0.100S", DurationFormatUtils.formatDurationISO(100L));
291         assertEquals("P0Y0M0DT0H1M15.321S", DurationFormatUtils.formatDurationISO(75321L));
292     }
293 
294     /**
295      * Tests that "1 &lt;unit&gt;s" gets converted to "1 &lt;unit&gt;" but that "11 &lt;unit&gt;s" is left alone.
296      */
297     @Test
298     void testFormatDurationPluralWords() {
299         final long oneSecond = 1000;
300         final long oneMinute = oneSecond * 60;
301         final long oneHour = oneMinute * 60;
302         final long oneDay = oneHour * 24;
303         String text;
304 
305         text = DurationFormatUtils.formatDurationWords(oneSecond, false, false);
306         assertEquals("0 days 0 hours 0 minutes 1 second", text);
307         text = DurationFormatUtils.formatDurationWords(oneSecond * 2, false, false);
308         assertEquals("0 days 0 hours 0 minutes 2 seconds", text);
309         text = DurationFormatUtils.formatDurationWords(oneSecond * 11, false, false);
310         assertEquals("0 days 0 hours 0 minutes 11 seconds", text);
311 
312         text = DurationFormatUtils.formatDurationWords(oneMinute, false, false);
313         assertEquals("0 days 0 hours 1 minute 0 seconds", text);
314         text = DurationFormatUtils.formatDurationWords(oneMinute * 2, false, false);
315         assertEquals("0 days 0 hours 2 minutes 0 seconds", text);
316         text = DurationFormatUtils.formatDurationWords(oneMinute * 11, false, false);
317         assertEquals("0 days 0 hours 11 minutes 0 seconds", text);
318         text = DurationFormatUtils.formatDurationWords(oneMinute + oneSecond, false, false);
319         assertEquals("0 days 0 hours 1 minute 1 second", text);
320 
321         text = DurationFormatUtils.formatDurationWords(oneHour, false, false);
322         assertEquals("0 days 1 hour 0 minutes 0 seconds", text);
323         text = DurationFormatUtils.formatDurationWords(oneHour * 2, false, false);
324         assertEquals("0 days 2 hours 0 minutes 0 seconds", text);
325         text = DurationFormatUtils.formatDurationWords(oneHour * 11, false, false);
326         assertEquals("0 days 11 hours 0 minutes 0 seconds", text);
327         text = DurationFormatUtils.formatDurationWords(oneHour + oneMinute + oneSecond, false, false);
328         assertEquals("0 days 1 hour 1 minute 1 second", text);
329 
330         text = DurationFormatUtils.formatDurationWords(oneDay, false, false);
331         assertEquals("1 day 0 hours 0 minutes 0 seconds", text);
332         text = DurationFormatUtils.formatDurationWords(oneDay * 2, false, false);
333         assertEquals("2 days 0 hours 0 minutes 0 seconds", text);
334         text = DurationFormatUtils.formatDurationWords(oneDay * 11, false, false);
335         assertEquals("11 days 0 hours 0 minutes 0 seconds", text);
336         text = DurationFormatUtils.formatDurationWords(oneDay + oneHour + oneMinute + oneSecond, false, false);
337         assertEquals("1 day 1 hour 1 minute 1 second", text);
338     }
339 
340     @Test
341     void testFormatDurationWords() {
342         String text;
343 
344         text = DurationFormatUtils.formatDurationWords(50 * 1000, true, false);
345         assertEquals("50 seconds", text);
346         text = DurationFormatUtils.formatDurationWords(65 * 1000, true, false);
347         assertEquals("1 minute 5 seconds", text);
348         text = DurationFormatUtils.formatDurationWords(120 * 1000, true, false);
349         assertEquals("2 minutes 0 seconds", text);
350         text = DurationFormatUtils.formatDurationWords(121 * 1000, true, false);
351         assertEquals("2 minutes 1 second", text);
352         text = DurationFormatUtils.formatDurationWords(72 * 60 * 1000, true, false);
353         assertEquals("1 hour 12 minutes 0 seconds", text);
354         text = DurationFormatUtils.formatDurationWords(24 * 60 * 60 * 1000, true, false);
355         assertEquals("1 day 0 hours 0 minutes 0 seconds", text);
356 
357         text = DurationFormatUtils.formatDurationWords(50 * 1000, true, true);
358         assertEquals("50 seconds", text);
359         text = DurationFormatUtils.formatDurationWords(65 * 1000, true, true);
360         assertEquals("1 minute 5 seconds", text);
361         text = DurationFormatUtils.formatDurationWords(120 * 1000, true, true);
362         assertEquals("2 minutes", text);
363         text = DurationFormatUtils.formatDurationWords(121 * 1000, true, true);
364         assertEquals("2 minutes 1 second", text);
365         text = DurationFormatUtils.formatDurationWords(72 * 60 * 1000, true, true);
366         assertEquals("1 hour 12 minutes", text);
367         text = DurationFormatUtils.formatDurationWords(24 * 60 * 60 * 1000, true, true);
368         assertEquals("1 day", text);
369 
370         text = DurationFormatUtils.formatDurationWords(50 * 1000, false, true);
371         assertEquals("0 days 0 hours 0 minutes 50 seconds", text);
372         text = DurationFormatUtils.formatDurationWords(65 * 1000, false, true);
373         assertEquals("0 days 0 hours 1 minute 5 seconds", text);
374         text = DurationFormatUtils.formatDurationWords(120 * 1000, false, true);
375         assertEquals("0 days 0 hours 2 minutes", text);
376         text = DurationFormatUtils.formatDurationWords(121 * 1000, false, true);
377         assertEquals("0 days 0 hours 2 minutes 1 second", text);
378         text = DurationFormatUtils.formatDurationWords(72 * 60 * 1000, false, true);
379         assertEquals("0 days 1 hour 12 minutes", text);
380         text = DurationFormatUtils.formatDurationWords(24 * 60 * 60 * 1000, false, true);
381         assertEquals("1 day", text);
382 
383         text = DurationFormatUtils.formatDurationWords(50 * 1000, false, false);
384         assertEquals("0 days 0 hours 0 minutes 50 seconds", text);
385         text = DurationFormatUtils.formatDurationWords(65 * 1000, false, false);
386         assertEquals("0 days 0 hours 1 minute 5 seconds", text);
387         text = DurationFormatUtils.formatDurationWords(120 * 1000, false, false);
388         assertEquals("0 days 0 hours 2 minutes 0 seconds", text);
389         text = DurationFormatUtils.formatDurationWords(121 * 1000, false, false);
390         assertEquals("0 days 0 hours 2 minutes 1 second", text);
391         text = DurationFormatUtils.formatDurationWords(72 * 60 * 1000, false, false);
392         assertEquals("0 days 1 hour 12 minutes 0 seconds", text);
393         text = DurationFormatUtils.formatDurationWords(24 * 60 * 60 * 1000 + 72 * 60 * 1000, false, false);
394         assertEquals("1 day 1 hour 12 minutes 0 seconds", text);
395         text = DurationFormatUtils.formatDurationWords(2 * 24 * 60 * 60 * 1000 + 72 * 60 * 1000, false, false);
396         assertEquals("2 days 1 hour 12 minutes 0 seconds", text);
397         for (int i = 2; i < 31; i++) {
398             text = DurationFormatUtils.formatDurationWords(i * 24 * 60 * 60 * 1000L, false, false);
399             assertEquals(i + " days 0 hours 0 minutes 0 seconds", text);
400         }
401     }
402 
403     @Test
404     void testFormatNegativeDuration() {
405         assertIllegalArgumentException(() -> DurationFormatUtils.formatDuration(-5000, "S", true));
406     }
407 
408     @Test
409     void testFormatNegativeDurationHMS() {
410         assertIllegalArgumentException(() -> DurationFormatUtils.formatDurationHMS(-5000));
411     }
412 
413     @Test
414     void testFormatNegativeDurationISO() {
415         assertIllegalArgumentException(() -> DurationFormatUtils.formatDurationISO(-5000));
416     }
417 
418     @Test
419     void testFormatNegativeDurationWords() {
420         assertIllegalArgumentException(() -> DurationFormatUtils.formatDurationWords(-5000, true, true));
421     }
422 
423     @Test
424     void testFormatPeriod() {
425         final Calendar cal1970 = Calendar.getInstance();
426         cal1970.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
427         cal1970.set(Calendar.MILLISECOND, 0);
428         final long time1970 = cal1970.getTime().getTime();
429 
430         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time1970, "y"));
431         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time1970, "M"));
432         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time1970, "d"));
433         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time1970, "H"));
434         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time1970, "m"));
435         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time1970, "s"));
436         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time1970, "S"));
437         assertEquals("0000", DurationFormatUtils.formatPeriod(time1970, time1970, "SSSS"));
438         assertEquals("0000", DurationFormatUtils.formatPeriod(time1970, time1970, "yyyy"));
439         assertEquals("0000", DurationFormatUtils.formatPeriod(time1970, time1970, "yyMM"));
440 
441         long time = time1970 + 60 * 1000;
442         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time, "y"));
443         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time, "M"));
444         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time, "d"));
445         assertEquals("0", DurationFormatUtils.formatPeriod(time1970, time, "H"));
446         assertEquals("1", DurationFormatUtils.formatPeriod(time1970, time, "m"));
447         assertEquals("60", DurationFormatUtils.formatPeriod(time1970, time, "s"));
448         assertEquals("60000", DurationFormatUtils.formatPeriod(time1970, time, "S"));
449         assertEquals("01:00", DurationFormatUtils.formatPeriod(time1970, time, "mm:ss"));
450 
451         final Calendar cal = Calendar.getInstance();
452         cal.set(1973, Calendar.JULY, 1, 0, 0, 0);
453         cal.set(Calendar.MILLISECOND, 0);
454         time = cal.getTime().getTime();
455         assertEquals("36", DurationFormatUtils.formatPeriod(time1970, time, "yM"));
456         assertEquals("3 years 6 months", DurationFormatUtils.formatPeriod(time1970, time, "y' years 'M' months'"));
457         assertEquals("03/06", DurationFormatUtils.formatPeriod(time1970, time, "yy/MM"));
458 
459         cal.set(1973, Calendar.NOVEMBER, 1, 0, 0, 0);
460         cal.set(Calendar.MILLISECOND, 0);
461         time = cal.getTime().getTime();
462         assertEquals("310", DurationFormatUtils.formatPeriod(time1970, time, "yM"));
463         assertEquals("3 years 10 months", DurationFormatUtils.formatPeriod(time1970, time, "y' years 'M' months'"));
464         assertEquals("03/10", DurationFormatUtils.formatPeriod(time1970, time, "yy/MM"));
465 
466         cal.set(1974, Calendar.JANUARY, 1, 0, 0, 0);
467         cal.set(Calendar.MILLISECOND, 0);
468         time = cal.getTime().getTime();
469         assertEquals("40", DurationFormatUtils.formatPeriod(time1970, time, "yM"));
470         assertEquals("4 'years 0 months", DurationFormatUtils.formatPeriod(time1970, time, "y' ''years' M 'months'"));
471         assertEquals("4 years 0 months", DurationFormatUtils.formatPeriod(time1970, time, "y' years 'M' months'"));
472         assertEquals("4years 0months", DurationFormatUtils.formatPeriod(time1970, time, "y'years 'M'months'"));
473         assertEquals("04/00", DurationFormatUtils.formatPeriod(time1970, time, "yy/MM"));
474         assertEquals("48", DurationFormatUtils.formatPeriod(time1970, time, "M"));
475         assertEquals("48", DurationFormatUtils.formatPeriod(time1970, time, "MM"));
476         assertEquals("048", DurationFormatUtils.formatPeriod(time1970, time, "MMM"));
477         // no date in result
478         assertEquals("hello", DurationFormatUtils.formatPeriod(time1970, time, "'hello'"));
479         assertEquals("hello'world", DurationFormatUtils.formatPeriod(time1970, time, "'hello''world'"));
480     }
481 
482     @Test
483     void testFormatPeriodeStartGreaterEnd() {
484         assertIllegalArgumentException(() -> DurationFormatUtils.formatPeriod(5000, 2500, "yy/MM"));
485     }
486 
487     @Test
488     void testFormatPeriodExtremeDatesWithOffsets() {
489         assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
490             final String format = "d H m s S";
491             final String expected = DurationFormatUtils.formatDuration(Long.MAX_VALUE, format);
492             for (final String zone : new String[] { "GMT", "GMT+14:00", "GMT-12:00" }) {
493                 final TimeZone timeZone = TimeZones.getTimeZone(zone);
494                 assertEquals(expected, DurationFormatUtils.formatPeriod(0, Long.MAX_VALUE, format, true, timeZone), zone);
495                 assertEquals(expected, DurationFormatUtils.formatPeriod(Long.MIN_VALUE, -1, format, true, timeZone), zone);
496                 assertEquals("213503982334", DurationFormatUtils.formatPeriod(Long.MIN_VALUE, Long.MAX_VALUE, "d", true, timeZone), zone);
497             }
498         });
499     }
500 
501     @SuppressWarnings("deprecation")
502     @Test
503     void testFormatPeriodISO() {
504         final TimeZone timeZone = TimeZones.getTimeZone("GMT-3");
505         final Calendar base = Calendar.getInstance(timeZone);
506         base.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
507         base.set(Calendar.MILLISECOND, 0);
508 
509         final Calendar cal = Calendar.getInstance(timeZone);
510         cal.set(2002, Calendar.FEBRUARY, 23, 9, 11, 12);
511         cal.set(Calendar.MILLISECOND, 1);
512         String text;
513         // repeat a test from testDateTimeISO to compare extended and not extended.
514         text = DateFormatUtils.format(cal, DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT.getPattern(), timeZone);
515         assertEquals("2002-02-23T09:11:12-03:00", text);
516         // test fixture is the same as above, but now with extended format.
517         text = DurationFormatUtils.formatPeriod(base.getTime().getTime(), cal.getTime().getTime(),
518                 DurationFormatUtils.ISO_EXTENDED_FORMAT_PATTERN, false, timeZone);
519         assertEquals("P32Y1M22DT9H11M12.001S", text);
520         // test fixture from example in https://www.w3.org/TR/xmlschema-2/#duration
521         cal.set(1971, Calendar.FEBRUARY, 3, 10, 30, 0);
522         cal.set(Calendar.MILLISECOND, 0);
523         text = DurationFormatUtils.formatPeriod(base.getTime().getTime(), cal.getTime().getTime(),
524                 DurationFormatUtils.ISO_EXTENDED_FORMAT_PATTERN, false, timeZone);
525         assertEquals("P1Y1M2DT10H30M0.000S", text);
526         // want a way to say 'don't print the seconds in format()' or other fields for that matter:
527         // assertEquals("P1Y2M3DT10H30M", text);
528     }
529 
530     @Test
531     void testFormatPeriodISOAcrossMonths() {
532         final TimeZone tz = TimeZones.getTimeZone("GMT-3");
533         // Verifies period formatting across a month boundary (Jan 31 to Feb 1)
534         final Calendar start = Calendar.getInstance(tz);
535         start.clear();
536         start.set(2020, Calendar.JANUARY, 31);
537         final Calendar end = Calendar.getInstance(tz);
538         end.clear();
539         end.set(2020, Calendar.FEBRUARY, 1);
540         final String result = DurationFormatUtils.formatPeriod(start.getTimeInMillis(), end.getTimeInMillis(), DurationFormatUtils.ISO_EXTENDED_FORMAT_PATTERN,
541                 false, tz);
542         assertEquals("P0Y0M1DT0H0M0.000S", result);
543     }
544 
545     @Test
546     void testFormatPeriodISOLeapYearBoundary() {
547         final TimeZone tz = TimeZones.getTimeZone("GMT-3");
548         // Verifies period formatting across a leap-year boundary (Feb 29 to Mar 1)
549         final Calendar start = Calendar.getInstance(tz);
550         start.clear();
551         start.set(2020, Calendar.FEBRUARY, 29);
552         final Calendar end = Calendar.getInstance(tz);
553         end.clear();
554         end.set(2020, Calendar.MARCH, 1);
555         final String result = DurationFormatUtils.formatPeriod(start.getTimeInMillis(), end.getTimeInMillis(), DurationFormatUtils.ISO_EXTENDED_FORMAT_PATTERN,
556                 false, tz);
557         assertEquals("P0Y0M1DT0H0M0.000S", result);
558     }
559 
560     @Test
561     void testFormatPeriodISOMethod() {
562         assertEquals("P0Y0M0DT0H0M0.000S", DurationFormatUtils.formatPeriodISO(0L, 0L));
563         assertEquals("P0Y0M0DT0H0M1.000S", DurationFormatUtils.formatPeriodISO(0L, 1000L));
564         assertEquals("P0Y0M0DT0H1M1.000S", DurationFormatUtils.formatPeriodISO(0L, 61000L));
565     }
566 
567     @Test
568     void testFormatPeriodISOStartGreaterEnd() {
569         assertIllegalArgumentException(() -> DurationFormatUtils.formatPeriodISO(5000, 2000));
570     }
571 
572     @Test
573     void testFormatPeriodLargeFunnelledValue() {
574         final TimeZone gmt = TimeZones.getTimeZone("GMT");
575         // ~69 years, chosen so the seconds count exceeds Integer.MAX_VALUE once days/hours/minutes are
576         // funnelled into the single requested field.
577         final long endMillis = 2_175_984_000_000L;
578         assertEquals("2175984000", DurationFormatUtils.formatPeriod(0, endMillis, "s", true, gmt));
579         assertEquals("2175984000000", DurationFormatUtils.formatPeriod(0, endMillis, "S", true, gmt));
580         // formatPeriod must agree with formatDuration, which reduces the same span in long arithmetic.
581         assertEquals(DurationFormatUtils.formatDuration(endMillis, "s"),
582                 DurationFormatUtils.formatPeriod(0, endMillis, "s", true, gmt));
583     }
584 
585     /**
586      * Leap-day boundaries across the year-normalization fast path (values verified against the previous year-by-year walk).
587      */
588     @Test
589     void testFormatPeriodLeapDayBoundaries() {
590         final TimeZone gmt = TimeZones.getTimeZone("GMT");
591         final Calendar leapDay = Calendar.getInstance(gmt);
592         leapDay.clear();
593         leapDay.set(2000, Calendar.FEBRUARY, 29, 0, 0, 0);
594         final Calendar feb28 = Calendar.getInstance(gmt);
595         feb28.clear();
596         feb28.set(2005, Calendar.FEBRUARY, 28, 0, 0, 0);
597         assertEquals("1826", DurationFormatUtils.formatPeriod(leapDay.getTimeInMillis(), feb28.getTimeInMillis(), "d", true, gmt));
598         final Calendar mar1 = Calendar.getInstance(gmt);
599         mar1.clear();
600         mar1.set(2005, Calendar.MARCH, 1, 0, 0, 0);
601         assertEquals("1827", DurationFormatUtils.formatPeriod(leapDay.getTimeInMillis(), mar1.getTimeInMillis(), "d", true, gmt));
602         final Calendar mar1999 = Calendar.getInstance(gmt);
603         mar1999.clear();
604         mar1999.set(1999, Calendar.MARCH, 1, 0, 0, 0);
605         assertEquals("2191", DurationFormatUtils.formatPeriod(mar1999.getTimeInMillis(), feb28.getTimeInMillis(), "d", true, gmt));
606     }
607 
608     @Test
609     void testFormatPeriodLeapDayWithTimeBorrowing() {
610         final TimeZone timeZone = TimeZones.getTimeZone("GMT");
611         final Calendar start = Calendar.getInstance(timeZone);
612         start.clear();
613         start.set(2000, Calendar.FEBRUARY, 29, 23, 59, 59);
614         start.set(Calendar.MILLISECOND, 999);
615         final Calendar end = Calendar.getInstance(timeZone);
616         end.clear();
617         end.set(2005, Calendar.FEBRUARY, 28);
618         assertEquals("1825 0 0 0 001", DurationFormatUtils.formatPeriod(start.getTimeInMillis(), end.getTimeInMillis(), "d H m s S", true, timeZone));
619     }
620 
621     @Test
622     void testFormatPeriodLocalDaysAcrossDaylightSaving() {
623         final TimeZone timeZone = TimeZones.getTimeZone("America/New_York");
624         final Calendar start = Calendar.getInstance(timeZone);
625         start.clear();
626         start.set(2024, Calendar.MARCH, 9, 12, 0, 0);
627         final Calendar end = (Calendar) start.clone();
628         end.add(Calendar.DAY_OF_MONTH, 1);
629         assertEquals(Duration.ofHours(23).toMillis(), end.getTimeInMillis() - start.getTimeInMillis());
630         assertEquals("1 0", DurationFormatUtils.formatPeriod(start.getTimeInMillis(), end.getTimeInMillis(), "d H", true, timeZone));
631         start.set(2024, Calendar.NOVEMBER, 2, 12, 0, 0);
632         end.setTimeInMillis(start.getTimeInMillis());
633         end.add(Calendar.DAY_OF_MONTH, 1);
634         assertEquals(Duration.ofHours(25).toMillis(), end.getTimeInMillis() - start.getTimeInMillis());
635         assertEquals("1 0", DurationFormatUtils.formatPeriod(start.getTimeInMillis(), end.getTimeInMillis(), "d H", true, timeZone));
636     }
637 
638     @Test
639     void testFormatPeriodLongRangeBounds() {
640         // A one-millisecond span sitting at the extremes of the long input range must still reduce
641         // correctly, confirming formatPeriod handles the whole range of millisecond inputs.
642         assertFormatPeriodOneMilli(Long.MAX_VALUE - 1, Long.MAX_VALUE);
643         assertFormatPeriodOneMilli(Long.MIN_VALUE, Long.MIN_VALUE + 1);
644         assertFormatPeriodOneMilli((long) Integer.MIN_VALUE - 1, Integer.MIN_VALUE);
645     }
646 
647     /**
648      * The no-y/no-M path used to normalize years by walking the calendar one year per loop iteration, making the cost linear in the span: this case took ~292
649      * million Calendar round trips. It must complete promptly and produce the exact day count.
650      */
651     @Test
652     void testFormatPeriodMaximumSpanCompletesQuickly() {
653         final TimeZone gmt = TimeZones.getTimeZone("GMT");
654         // floor(Long.MAX_VALUE / MILLIS_PER_DAY) days from the epoch, with time-of-day remainders that never borrow.
655         assertTimeoutPreemptively(Duration.ofSeconds(5),
656                 () -> assertEquals("106751991167", DurationFormatUtils.formatPeriod(0, Long.MAX_VALUE, "d", true, gmt)));
657     }
658 
659     @Test
660     void testFormatPeriodWithoutMonths() {
661         final TimeZone timeZone = TimeZone.getTimeZone("UTC");
662         final Calendar start = Calendar.getInstance(timeZone);
663         start.set(2024, Calendar.DECEMBER, 15, 0, 0, 0);
664         start.set(Calendar.MILLISECOND, 0);
665 
666         final Calendar end = Calendar.getInstance(timeZone);
667         end.set(2025, Calendar.JANUARY, 15, 0, 0, 0);
668         end.set(Calendar.MILLISECOND, 0);
669 
670         // 31 days elapsed across year boundary
671         assertEquals("0 years 31 days", DurationFormatUtils.formatPeriod(start.getTimeInMillis(), end.getTimeInMillis(), "y' years 'd' days'", false, timeZone));
672         assertEquals("0y 31d", DurationFormatUtils.formatPeriod(start.getTimeInMillis(), end.getTimeInMillis(), "y'y 'd'd'", false, timeZone));
673 
674         // 361 days elapsed (less than 1 full year)
675         start.set(2024, Calendar.JANUARY, 15, 0, 0, 0);
676         end.set(2025, Calendar.JANUARY, 10, 0, 0, 0);
677         assertEquals("0 years 361 days", DurationFormatUtils.formatPeriod(start.getTimeInMillis(), end.getTimeInMillis(), "y' years 'd' days'", false, timeZone));
678 
679         // Leap year to non-leap year (Feb 29, 2024 to Feb 28, 2025 = 365 days)
680         start.set(2024, Calendar.FEBRUARY, 29, 0, 0, 0);
681         end.set(2025, Calendar.FEBRUARY, 28, 0, 0, 0);
682         assertEquals("0 years 365 days", DurationFormatUtils.formatPeriod(start.getTimeInMillis(), end.getTimeInMillis(), "y' years 'd' days'", false, timeZone));
683     }
684 
685     @Test
686     void testFormatPeriodWithoutMonthsAfterLeapDayAnniversary() {
687         final TimeZone timeZone = TimeZone.getTimeZone("UTC");
688         final Calendar start = Calendar.getInstance(timeZone);
689         start.clear();
690         // 2020 was a leap year
691         start.set(2020, Calendar.FEBRUARY, 29);
692         final Calendar end = Calendar.getInstance(timeZone);
693         end.clear();
694         // 2021 was not a leap year
695         end.set(2021, Calendar.MARCH, 1);
696         assertEquals("1 years 1 days", DurationFormatUtils.formatPeriod(start.getTimeInMillis(), end.getTimeInMillis(), "y' years 'd' days'", false, timeZone));
697     }
698 
699     /**
700      * Takes 8 seconds to run.
701      */
702     @Test
703     void testFourYears() {
704         final Calendar c = Calendar.getInstance();
705         c.set(2004, 0, 1, 0, 0, 0);
706         for (int i = 0; i < FOUR_YEARS; i++) {
707             bruteForce(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH), "d", Calendar.DAY_OF_MONTH);
708             c.add(Calendar.DAY_OF_MONTH, 1);
709         }
710     }
711 
712     // https://issues.apache.org/jira/browse/LANG-281
713     @Test
714     void testJiraLang281() {
715         assertEqualDuration("09", new int[] { 2005, 11, 31, 0, 0, 0 }, new int[] { 2006, 9, 6, 0, 0, 0 }, "MM");
716     }
717 
718     @ParameterizedTest
719     @MethodSource
720     void testLANG1827(final String label, final String expected, final long durationMillis, final String format) {
721         assertEquals(expected, DurationFormatUtils.formatDuration(durationMillis, format), label);
722     }
723 
724     @Test
725     void testLANG1827UnmatchedQuote() {
726         assertIllegalArgumentException(() -> DurationFormatUtils.lexx("'unmatched"));
727     }
728 
729     @Test
730     void testLANG815() {
731         final Calendar calendar = Calendar.getInstance();
732         calendar.set(2012, Calendar.JULY, 30, 0, 0, 0);
733         final long startMillis = calendar.getTimeInMillis();
734 
735         calendar.set(2012, Calendar.SEPTEMBER, 8);
736         final long endMillis = calendar.getTimeInMillis();
737 
738         assertEquals("1 9", DurationFormatUtils.formatPeriod(startMillis, endMillis, "M d"));
739     }
740 
741     @Test
742     void testLANG981() { // unmatched quote char in lexx
743         assertIllegalArgumentException(() -> DurationFormatUtils.lexx("'yMdHms''S"));
744     }
745 
746     @Test
747     void testLANG982() { // More than 3 millisecond digits following a second
748         assertEquals("61.999", DurationFormatUtils.formatDuration(61999, "s.S"));
749         assertEquals("1 1999", DurationFormatUtils.formatDuration(61999, "m S"));
750         assertEquals("61.999", DurationFormatUtils.formatDuration(61999, "s.SSS"));
751         assertEquals("1 1999", DurationFormatUtils.formatDuration(61999, "m SSS"));
752         assertEquals("61.0999", DurationFormatUtils.formatDuration(61999, "s.SSSS"));
753         assertEquals("1 1999", DurationFormatUtils.formatDuration(61999, "m SSSS"));
754         assertEquals("61.00999", DurationFormatUtils.formatDuration(61999, "s.SSSSS"));
755         assertEquals("1 01999", DurationFormatUtils.formatDuration(61999, "m SSSSS"));
756     }
757 
758     @Test
759     void testLANG984() { // Long durations
760         assertEquals("0", DurationFormatUtils.formatDuration(0, "S"));
761         assertEquals(Integer.toString(Integer.MAX_VALUE), DurationFormatUtils.formatDuration(Integer.MAX_VALUE, "S"));
762         long maxIntPlus = Integer.MAX_VALUE;
763         maxIntPlus++;
764         assertEquals(Long.toString(maxIntPlus), DurationFormatUtils.formatDuration(maxIntPlus, "S"));
765         assertEquals(Long.toString(Long.MAX_VALUE), DurationFormatUtils.formatDuration(Long.MAX_VALUE, "S"));
766     }
767 
768     @Test
769     void testLexx() {
770         // tests each constant
771         assertArrayEquals(new DurationFormatUtils.Token[] {
772             createTokenWithCount(DurationFormatUtils.y, 1),
773             createTokenWithCount(DurationFormatUtils.M, 1),
774             createTokenWithCount(DurationFormatUtils.d, 1),
775             createTokenWithCount(DurationFormatUtils.H, 1),
776             createTokenWithCount(DurationFormatUtils.m, 1),
777             createTokenWithCount(DurationFormatUtils.s, 1),
778             createTokenWithCount(DurationFormatUtils.S, 1) }, DurationFormatUtils.lexx("yMdHmsS"));
779         // tests the ISO 8601-like
780         assertArrayEquals(new DurationFormatUtils.Token[] {
781             createTokenWithCount(DurationFormatUtils.H, 2),
782             createTokenWithCount(new StringBuilder(":"), 1),
783             createTokenWithCount(DurationFormatUtils.m, 2),
784             createTokenWithCount(new StringBuilder(":"), 1),
785             createTokenWithCount(DurationFormatUtils.s, 2),
786             createTokenWithCount(new StringBuilder("."), 1),
787             createTokenWithCount(DurationFormatUtils.S, 3) }, DurationFormatUtils.lexx("HH:mm:ss.SSS"));
788         // test the iso extended format
789         assertArrayEquals(new DurationFormatUtils.Token[] {
790             createTokenWithCount(new StringBuilder("P"), 1),
791             createTokenWithCount(DurationFormatUtils.y, 4),
792             createTokenWithCount(new StringBuilder("Y"), 1),
793             createTokenWithCount(DurationFormatUtils.M, 1),
794             createTokenWithCount(new StringBuilder("M"), 1),
795             createTokenWithCount(DurationFormatUtils.d, 1),
796             createTokenWithCount(new StringBuilder("DT"), 1),
797             createTokenWithCount(DurationFormatUtils.H, 1),
798             createTokenWithCount(new StringBuilder("H"), 1),
799             createTokenWithCount(DurationFormatUtils.m, 1),
800             createTokenWithCount(new StringBuilder("M"), 1),
801             createTokenWithCount(DurationFormatUtils.s, 1),
802             createTokenWithCount(new StringBuilder("."), 1),
803             createTokenWithCount(DurationFormatUtils.S, 3),
804             createTokenWithCount(new StringBuilder("S"), 1) }, DurationFormatUtils.lexx(DurationFormatUtils.ISO_EXTENDED_FORMAT_PATTERN));
805         // test failures in equals
806         final DurationFormatUtils.Token token = createTokenWithCount(DurationFormatUtils.y, 4);
807         assertEquals(token, token);
808         assertEquals(token.hashCode(), token.hashCode());
809         assertNotEquals(token, new Object(), "Token equal to non-Token class.");
810         final Token token2 = createTokenWithCount("", 1);
811         assertNotEquals(token, token2, "Token equal to Token with wrong value class.");
812         assertNotEquals(token.hashCode(), token2.hashCode());
813         assertNotEquals(token, createTokenWithCount(DurationFormatUtils.y, 1), "Token equal to Token with different count.");
814         final DurationFormatUtils.Token numToken = createTokenWithCount("1", 4);
815         assertEquals(numToken, numToken, "Token with Number value not equal to itself.");
816     }
817 
818     @Test
819     void testLiteralPrefixOptionalToken() {
820         assertEquals(DurationFormatUtils.formatDuration(10000L, "s's'"), DurationFormatUtils.formatDuration(10000L, "['['d']']['<'H'>']['{'m'}']s's'"));
821         assertEquals(DurationFormatUtils.formatDuration(10000L, "s's'"), DurationFormatUtils.formatDuration(10000L, "['{'m'}']s's'"));
822     }
823 
824     // Testing the under a day range in DurationFormatUtils.formatPeriod
825     @Test
826     void testLowDurations() {
827         for (int hr = 0; hr < 24; hr++) {
828             for (int min = 0; min < 60; min++) {
829                 for (int sec = 0; sec < 60; sec++) {
830                     assertEqualDuration(hr + ":" + min + ":" + sec, new int[] { 2000, 0, 1, 0, 0, 0, 0 }, new int[] { 2000, 0, 1, hr, min, sec }, "H:m:s");
831                 }
832             }
833         }
834     }
835 
836     @Test
837     void testMultipleOptionalBlocks() {
838         assertEquals(DurationFormatUtils.formatDuration(Duration.ofHours(1).toMillis(), "'[['H']]'"),
839                 DurationFormatUtils.formatDuration(Duration.ofHours(1).toMillis(), "['{'d'}']['[['H']]']"));
840         assertEquals(DurationFormatUtils.formatDuration(Duration.ofDays(1).toMillis(), "['{'d'}']"),
841                 DurationFormatUtils.formatDuration(Duration.ofDays(1).toMillis(), "['{'d'}']['['H']']"));
842     }
843 
844     @Test
845     void testOptionalLiteralSpecialCharacters() {
846       assertEquals(
847           DurationFormatUtils.formatDuration(10000L, "s's'"),
848           DurationFormatUtils.formatDuration(10000L, "['['m']']s's'"));
849     }
850 
851     @Test
852     void testOptionalToken() {
853         // make sure optional formats match corresponding adjusted non-optional formats
854         assertEquals(
855                 DurationFormatUtils.formatDuration(915361000L, "d'd'H'h'm'm's's'"),
856                 DurationFormatUtils.formatDuration(915361000L, "[d'd'H'h'm'm']s's'"));
857         assertEquals(
858                 DurationFormatUtils.formatDuration(9153610L, "H'h'm'm's's'"),
859                 DurationFormatUtils.formatDuration(9153610L, "[d'd'H'h'm'm']s's'"));
860         assertEquals(
861                 DurationFormatUtils.formatDuration(915361L, "m'm's's'"),
862                 DurationFormatUtils.formatDuration(915361L, "[d'd'H'h'm'm']s's'"));
863         assertEquals(
864                 DurationFormatUtils.formatDuration(9153L, "s's'"),
865                 DurationFormatUtils.formatDuration(9153L, "[d'd'H'h'm'm']s's'"));
866         assertEquals(
867                 DurationFormatUtils.formatDuration(9153L, "s's'"),
868                 DurationFormatUtils.formatDuration(9153L, "[d'd'H'h'm'm']s's'"));
869         assertEquals(
870                 DurationFormatUtils.formatPeriod(9153610L, 915361000L, "d'd'H'h'm'm's's'"),
871                 DurationFormatUtils.formatPeriod(9153610L, 915361000L, "[d'd'H'h'm'm']s's'"));
872         assertEquals(
873                 DurationFormatUtils.formatPeriod(915361L, 9153610L, "H'h'm'm's's'"),
874                 DurationFormatUtils.formatPeriod(915361L, 9153610L, "[d'd'H'h'm'm']s's'"));
875         assertEquals(
876                 DurationFormatUtils.formatPeriod(9153L, 915361L, "m'm's's'"),
877                 DurationFormatUtils.formatPeriod(9153L, 915361L, "[d'd'H'h'm'm']s's'"));
878         assertEquals(
879                 DurationFormatUtils.formatPeriod(0L, 9153L, "s's'"),
880                 DurationFormatUtils.formatPeriod(0L, 9153L, "[d'd'H'h'm'm']s's'"));
881         // make sure optional parts are actually omitted when zero
882         assertEquals("2h32m33s610ms", DurationFormatUtils.formatDuration(9153610L, "[d'd'H'h'm'm's's']S'ms'"));
883         assertEquals("15m15s361ms", DurationFormatUtils.formatDuration(915361L, "[d'd'H'h'm'm's's']S'ms'"));
884         assertEquals("9s153ms", DurationFormatUtils.formatDuration(9153L, "[d'd'H'h'm'm's's']S'ms'"));
885         assertEquals("915ms", DurationFormatUtils.formatDuration(915L, "[d'd'H'h'm'm's's']S'ms'"));
886         // make sure we can handle omitting multiple literals after a token
887         assertEquals(
888                 DurationFormatUtils.formatPeriod(915361L, 9153610L, "H'h''h2'm'm's's'"),
889                 DurationFormatUtils.formatPeriod(915361L, 9153610L, "[d'd''d2'H'h''h2'm'm']s's'"));
890     }
891 
892     @Test
893     void testRepeatedTokenSeparatedByLiteral() {
894         final long fiveHours = Duration.ofHours(5).toMillis();
895         // the same field letter on either side of a literal are two separate fields, not one padded field
896         assertEquals("5x5", DurationFormatUtils.formatDuration(fiveHours, "HxH", false));
897         assertEquals("5x5", DurationFormatUtils.formatDuration(fiveHours, "H'x'H", false));
898         // each H is a single-width field, so padding does not turn either into "05"
899         assertEquals("5x5", DurationFormatUtils.formatDuration(fiveHours, "HxH", true));
900         // separated by an optional block
901         assertEquals("5x5", DurationFormatUtils.formatDuration(fiveHours, "H[x]H", false));
902         assertEquals("2-2", DurationFormatUtils.formatDuration(Duration.ofDays(2).toMillis(), "d-d", false));
903         // adjacent repeats still collapse into a single padded field
904         assertEquals("05", DurationFormatUtils.formatDuration(fiveHours, "HH", true));
905     }
906 
907     @Test
908     void testUnmatchedOptionalTokens() {
909         assertIllegalArgumentException(() -> DurationFormatUtils.formatDuration(1, "[s"));
910         assertIllegalArgumentException(() -> DurationFormatUtils.formatDuration(1, "[[s"));
911         assertIllegalArgumentException(() -> DurationFormatUtils.formatDuration(1, "[s]]"));
912     }
913 
914 }