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    *      http://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  package org.apache.juneau.commons.time;
18  
19  import static org.apache.juneau.TestUtils.*;
20  import static org.junit.jupiter.api.Assertions.*;
21  
22  import java.time.*;
23  import java.time.temporal.*;
24  import java.util.Date;
25  
26  import org.apache.juneau.*;
27  import org.apache.juneau.utest.utils.FakeTimeProvider;
28  import org.junit.jupiter.api.*;
29  import org.junit.jupiter.params.*;
30  import org.junit.jupiter.params.provider.*;
31  
32  /**
33   * Tests for {@link GranularZonedDateTime}.
34   */
35  class GranularZonedDateTime_Test extends TestBase {
36  
37  	//====================================================================================================
38  	// Constructor(ZonedDateTime, ChronoField) tests
39  	//====================================================================================================
40  
41  	@Test
42  	void b01_constructorWithZonedDateTime() {
43  		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, ZoneId.of("UTC"));
44  		var gdt = new GranularZonedDateTime(zdt, ChronoField.HOUR_OF_DAY);
45  		assertEquals(zdt, gdt.zdt);
46  		assertEquals(ChronoField.HOUR_OF_DAY, gdt.precision);
47  	}
48  
49  	@Test
50  	void b02_constructorWithZonedDateTime_preservesZone() {
51  		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, ZoneId.of("America/New_York"));
52  		var gdt = new GranularZonedDateTime(zdt, ChronoField.HOUR_OF_DAY);
53  		assertEquals(ZoneId.of("America/New_York"), gdt.zdt.getZone());
54  	}
55  
56  	@Test
57  	void b03_constructorWithZonedDateTime_preservesPrecision() {
58  		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, ZoneId.of("UTC"));
59  		var gdt = new GranularZonedDateTime(zdt, ChronoField.MINUTE_OF_HOUR);
60  		assertEquals(ChronoField.MINUTE_OF_HOUR, gdt.precision);
61  	}
62  
63  	@Test
64  	void b04_ofZonedDateTime() {
65  		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, ZoneId.of("UTC"));
66  		var gdt = GranularZonedDateTime.of(zdt, ChronoField.HOUR_OF_DAY);
67  		assertEquals(zdt, gdt.zdt);
68  		assertEquals(ChronoField.HOUR_OF_DAY, gdt.precision);
69  	}
70  
71  	@Test
72  	void b05_ofDate() {
73  		var date = new Date(1705322445000L); // 2024-01-15T12:30:45Z
74  		var gdt = GranularZonedDateTime.of(date, ChronoField.SECOND_OF_MINUTE);
75  		assertEquals(ChronoField.SECOND_OF_MINUTE, gdt.precision);
76  		// Verify the instant is correct (date uses system default timezone)
77  		assertEquals(date.toInstant(), gdt.zdt.toInstant());
78  	}
79  
80  	@Test
81  	void b06_ofDateWithZoneId() {
82  		var date = new Date(1705322445000L); // 2024-01-15T12:30:45Z
83  		var zoneId = ZoneId.of("America/New_York");
84  		var gdt = GranularZonedDateTime.of(date, ChronoField.SECOND_OF_MINUTE, zoneId);
85  		assertEquals(ChronoField.SECOND_OF_MINUTE, gdt.precision);
86  		assertEquals(zoneId, gdt.zdt.getZone());
87  		assertEquals(2024, gdt.zdt.getYear());
88  		assertEquals(1, gdt.zdt.getMonthValue());
89  		assertEquals(15, gdt.zdt.getDayOfMonth());
90  	}
91  
92  	//====================================================================================================
93  	// copy() tests
94  	//====================================================================================================
95  
96  	@Test
97  	void c01_copy() {
98  		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, ZoneId.of("UTC"));
99  		var gdt1 = new GranularZonedDateTime(zdt, ChronoField.HOUR_OF_DAY);
100 		var gdt2 = gdt1.copy();
101 		assertNotSame(gdt1, gdt2);
102 		assertEquals(gdt1.zdt, gdt2.zdt);
103 		assertEquals(gdt1.precision, gdt2.precision);
104 	}
105 
106 	//====================================================================================================
107 	// getZonedDateTime() tests
108 	//====================================================================================================
109 
110 	@Test
111 	void d01_getZonedDateTime() {
112 		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, ZoneId.of("UTC"));
113 		var gdt = new GranularZonedDateTime(zdt, ChronoField.HOUR_OF_DAY);
114 		assertEquals(zdt, gdt.getZonedDateTime());
115 	}
116 
117 	@Test
118 	void d02_getPrecision() {
119 		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, ZoneId.of("UTC"));
120 		var gdt = new GranularZonedDateTime(zdt, ChronoField.MINUTE_OF_HOUR);
121 		assertEquals(ChronoField.MINUTE_OF_HOUR, gdt.getPrecision());
122 	}
123 
124 	//====================================================================================================
125 	// roll(ChronoField, int) tests
126 	//====================================================================================================
127 
128 	@Test
129 	void e01_roll_withField() {
130 		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 123000000, ZoneId.of("UTC"));
131 		var gdt = new GranularZonedDateTime(zdt, ChronoField.MILLI_OF_SECOND);
132 
133 		var rolled1 = gdt.roll(ChronoField.YEAR, 1);
134 		assertEquals(zdt.plusYears(1), rolled1.zdt);
135 
136 		var rolled2 = gdt.roll(ChronoField.MONTH_OF_YEAR, 1);
137 		assertEquals(zdt.plusMonths(1), rolled2.zdt);
138 
139 		var rolled3 = gdt.roll(ChronoField.DAY_OF_MONTH, 1);
140 		assertEquals(zdt.plusDays(1), rolled3.zdt);
141 
142 		var rolled4 = gdt.roll(ChronoField.HOUR_OF_DAY, 1);
143 		assertEquals(zdt.plusHours(1), rolled4.zdt);
144 
145 		var rolled5 = gdt.roll(ChronoField.MINUTE_OF_HOUR, 1);
146 		assertEquals(zdt.plusMinutes(1), rolled5.zdt);
147 
148 		var rolled6 = gdt.roll(ChronoField.SECOND_OF_MINUTE, 1);
149 		assertEquals(zdt.plusSeconds(1), rolled6.zdt);
150 
151 		var rolled7 = gdt.roll(ChronoField.MILLI_OF_SECOND, 1);
152 		assertEquals(zdt.plus(1, ChronoUnit.MILLIS), rolled7.zdt);
153 
154 		var rolled8 = gdt.roll(ChronoField.NANO_OF_SECOND, 1);
155 		assertEquals(zdt.plus(1, ChronoUnit.NANOS), rolled8.zdt);
156 	}
157 
158 	@Test
159 	void e02_roll_withNanoOfSecondPrecision() {
160 		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 123456789, ZoneId.of("UTC"));
161 		var gdt = new GranularZonedDateTime(zdt, ChronoField.NANO_OF_SECOND);
162 
163 		var rolled = gdt.roll(ChronoField.NANO_OF_SECOND, 100);
164 		assertEquals(zdt.plus(100, ChronoUnit.NANOS), rolled.zdt);
165 		assertEquals(ChronoField.NANO_OF_SECOND, rolled.precision);
166 	}
167 
168 	@Test
169 	void e03_roll_withUnsupportedField() {
170 		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, ZoneId.of("UTC"));
171 		var gdt = new GranularZonedDateTime(zdt, ChronoField.HOUR_OF_DAY);
172 
173 		assertThrowsWithMessage(IllegalArgumentException.class,
174 			"Unsupported roll field: AmPmOfDay",
175 			() -> gdt.roll(ChronoField.AMPM_OF_DAY, 1));
176 	}
177 
178 	//====================================================================================================
179 	// roll(int) tests
180 	//====================================================================================================
181 
182 	@Test
183 	void f01_roll_withPrecision() {
184 		var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, ZoneId.of("UTC"));
185 		var gdt = new GranularZonedDateTime(zdt, ChronoField.HOUR_OF_DAY);
186 		var rolled = gdt.roll(2);
187 		assertEquals(zdt.plusHours(2), rolled.zdt);
188 		assertEquals(ChronoField.HOUR_OF_DAY, rolled.precision);
189 	}
190 
191 	//====================================================================================================
192 	// Integration tests
193 	//====================================================================================================
194 
195 	@Test
196 	void h01_rollAndof() {
197 		var gdt1 = GranularZonedDateTime.of("2011", FAKE_TIME_PROVIDER);
198 		var gdt2 = gdt1.roll(1);
199 		assertEquals(2012, gdt2.zdt.getYear());
200 		assertEquals(ChronoField.YEAR, gdt2.precision);
201 	}
202 
203 	@Test
204 	void h02_rollMultipleTimes() {
205 		var gdt = GranularZonedDateTime.of("2011-01-15", FAKE_TIME_PROVIDER);
206 		var rolled1 = gdt.roll(1);
207 		var rolled2 = rolled1.roll(1);
208 		assertEquals(17, rolled2.zdt.getDayOfMonth());
209 	}
210 
211 	@Test
212 	void h03_rollWithDifferentField() {
213 		var gdt = GranularZonedDateTime.of("2011-01-15T12:30Z", FAKE_TIME_PROVIDER);
214 		// Roll by hours even though precision is minutes
215 		var rolled = gdt.roll(ChronoField.HOUR_OF_DAY, 2);
216 		assertEquals(14, rolled.zdt.getHour());
217 		assertEquals(30, rolled.zdt.getMinute());
218 		assertEquals(ChronoField.MINUTE_OF_HOUR, rolled.precision); // Original precision preserved
219 	}
220 
221 	@Test
222 	void h04_copyAndRoll() {
223 		var gdt1 = GranularZonedDateTime.of("2011-01-15", FAKE_TIME_PROVIDER);
224 		var gdt2 = gdt1.copy();
225 		var rolled = gdt2.roll(1);
226 		// Original should be unchanged
227 		assertEquals(15, gdt1.zdt.getDayOfMonth());
228 		assertEquals(16, rolled.zdt.getDayOfMonth());
229 	}
230 
231 	//====================================================================================================
232 	// of(String) and of(String, ZoneId) tests - see J_parserTests nested class below
233 	//====================================================================================================
234 
235 	private static final FakeTimeProvider FAKE_TIME_PROVIDER = new FakeTimeProvider();
236 
237 	@Nested class J_parserTests extends TestBase {
238 
239 		record ParseTest(int index, String name, String input, String expected, ZoneId defaultZoneId) {
240 			ParseTest(int index, String name, String input, String expected) {
241 				this(index, name, input, expected, null);
242 			}
243 		}
244 
245 		static ParseTest[] parseTests() {
246 			return new ParseTest[] {
247 				// Date formats
248 				new ParseTest(1, "yearOnly", "2011", "2011-01-01T00:00:00Z(Year)"),
249 				new ParseTest(2, "yearMonth", "2011-01", "2011-01-01T00:00:00Z(MonthOfYear)"),
250 				new ParseTest(3, "date", "2011-01-15", "2011-01-15T00:00:00Z(DayOfMonth)"),
251 
252 				// DateTime formats
253 				new ParseTest(4, "dateTime_hour", "2011-01-15T12", "2011-01-15T12:00:00Z(HourOfDay)"),
254 				new ParseTest(5, "dateTime_minute", "2011-01-15T12:30", "2011-01-15T12:30:00Z(MinuteOfHour)"),
255 				new ParseTest(6, "dateTime_second", "2011-01-15T12:30:45", "2011-01-15T12:30:45Z(SecondOfMinute)"),
256 
257 				// With fractional seconds
258 				new ParseTest(7, "dateTime_millisecond_dot", "2011-01-15T12:30:45.123", "2011-01-15T12:30:45.123Z(MilliOfSecond)"),
259 				new ParseTest(8, "dateTime_millisecond_comma", "2011-01-15T12:30:45,123", "2011-01-15T12:30:45.123Z(MilliOfSecond)"),
260 				new ParseTest(9, "dateTime_nanosecond_1digit", "2011-01-15T12:30:45.1", "2011-01-15T12:30:45.1Z(MilliOfSecond)"),
261 				new ParseTest(10, "dateTime_nanosecond_2digits", "2011-01-15T12:30:45.12", "2011-01-15T12:30:45.12Z(MilliOfSecond)"),
262 				new ParseTest(11, "dateTime_nanosecond_3digits", "2011-01-15T12:30:45.123", "2011-01-15T12:30:45.123Z(MilliOfSecond)"),
263 				new ParseTest(12, "dateTime_nanosecond_4digits", "2011-01-15T12:30:45.1234", "2011-01-15T12:30:45.1234Z(NanoOfSecond)"),
264 				new ParseTest(13, "dateTime_nanosecond_5digits", "2011-01-15T12:30:45.12345", "2011-01-15T12:30:45.12345Z(NanoOfSecond)"),
265 				new ParseTest(14, "dateTime_nanosecond_6digits", "2011-01-15T12:30:45.123456", "2011-01-15T12:30:45.123456Z(NanoOfSecond)"),
266 				new ParseTest(15, "dateTime_nanosecond_7digits", "2011-01-15T12:30:45.1234567", "2011-01-15T12:30:45.1234567Z(NanoOfSecond)"),
267 				new ParseTest(16, "dateTime_nanosecond_8digits", "2011-01-15T12:30:45.12345678", "2011-01-15T12:30:45.12345678Z(NanoOfSecond)"),
268 				new ParseTest(17, "dateTime_nanosecond_9digits", "2011-01-15T12:30:45.123456789", "2011-01-15T12:30:45.123456789Z(NanoOfSecond)"),
269 
270 				// Time-only formats (use fixed current time: 2000-01-01T12:00:00Z)
271 				new ParseTest(18, "timeOnly_hour", "T12", "2000-01-01T12:00:00Z(HourOfDay)"),
272 				new ParseTest(19, "timeOnly_minute", "T12:30", "2000-01-01T12:30:00Z(MinuteOfHour)"),
273 				new ParseTest(20, "timeOnly_second", "T12:30:45", "2000-01-01T12:30:45Z(SecondOfMinute)"),
274 				new ParseTest(21, "timeOnly_millisecond", "T12:30:45.123", "2000-01-01T12:30:45.123Z(MilliOfSecond)"),
275 				new ParseTest(22, "timeOnly_withZ", "T12:30:45Z", "2000-01-01T12:30:45Z(SecondOfMinute)"),
276 				new ParseTest(23, "timeOnly_withOffset", "T12:30:45+05:30", "2000-01-01T12:30:45+05:30(SecondOfMinute)"),
277 
278 				// With UTC timezone
279 				new ParseTest(24, "withZ", "2011-01-15T12:30:45Z", "2011-01-15T12:30:45Z(SecondOfMinute)"),
280 				new ParseTest(25, "yearWithZ", "2011Z", "2011-01-01T00:00:00Z(Year)"),
281 				new ParseTest(26, "yearMonthWithZ", "2011-01Z", "2011-01-01T00:00:00Z(MonthOfYear)"),
282 				new ParseTest(27, "dateWithZ", "2011-01-15Z", "2011-01-15T00:00:00Z(DayOfMonth)"),
283 				new ParseTest(28, "hourWithZ", "2011-01-15T12Z", "2011-01-15T12:00:00Z(HourOfDay)"),
284 				new ParseTest(29, "minuteWithZ", "2011-01-15T12:30Z", "2011-01-15T12:30:00Z(MinuteOfHour)"),
285 				new ParseTest(30, "millisecondWithZ", "2011-01-15T12:30:45.123Z", "2011-01-15T12:30:45.123Z(MilliOfSecond)"),
286 
287 				// With offset (hours only)
288 				new ParseTest(31, "offset_plusHH", "2011-01-15T12:30:45+05", "2011-01-15T12:30:45+05:00(SecondOfMinute)"),
289 				new ParseTest(32, "offset_minusHH", "2011-01-15T12:30:45-05", "2011-01-15T12:30:45-05:00(SecondOfMinute)"),
290 
291 				// With offset (hours and minutes, compact)
292 				new ParseTest(33, "offset_plusHHMM", "2011-01-15T12:30:45+0530", "2011-01-15T12:30:45+05:30(SecondOfMinute)"),
293 				new ParseTest(34, "offset_minusHHMM", "2011-01-15T12:30:45-0530", "2011-01-15T12:30:45-05:30(SecondOfMinute)"),
294 
295 				// With offset (hours and minutes, with colon)
296 				new ParseTest(35, "offset_plusHH_MM", "2011-01-15T12:30:45+05:30", "2011-01-15T12:30:45+05:30(SecondOfMinute)"),
297 				new ParseTest(36, "offset_minusHH_MM", "2011-01-15T12:30:45-05:30", "2011-01-15T12:30:45-05:30(SecondOfMinute)"),
298 				new ParseTest(37, "offset_minus12_30", "2011-01-15T12:30:45-12:30", "2011-01-15T12:30:45-12:30(SecondOfMinute)"),
299 				new ParseTest(38, "offset_plus09_00", "2011-01-15T12:30:45+09:00", "2011-01-15T12:30:45+09:00(SecondOfMinute)"),
300 
301 				// Timezone after various components
302 				new ParseTest(39, "yearFollowedByT", "2011T12", "2011-01-01T12:00:00Z(HourOfDay)"),
303 				new ParseTest(40, "yearFollowedByPlus", "2011+05", "2011-01-01T00:00:00+05:00(Year)"),
304 				new ParseTest(41, "yearFollowedByMinus", "2011-01-15T12-05", "2011-01-15T12:00:00-05:00(HourOfDay)"),
305 				new ParseTest(42, "monthFollowedByZ", "2011-01Z", "2011-01-01T00:00:00Z(MonthOfYear)"),
306 				new ParseTest(43, "monthFollowedByPlus", "2011-01+05", "2011-01-01T00:00:00+05:00(MonthOfYear)"),
307 				new ParseTest(44, "monthFollowedByMinus", "2011-01-15T12:30-05", "2011-01-15T12:30:00-05:00(MinuteOfHour)"),
308 				new ParseTest(45, "dayFollowedByZ", "2011-01-15Z", "2011-01-15T00:00:00Z(DayOfMonth)"),
309 				new ParseTest(46, "dayFollowedByPlus", "2011-01-15+05", "2011-01-15T00:00:00+05:00(DayOfMonth)"),
310 				new ParseTest(47, "dayFollowedByMinus", "2011-01-15-05", "2011-01-15T00:00:00-05:00(DayOfMonth)"),
311 				new ParseTest(48, "hourFollowedByZ", "2011-01-15T12Z", "2011-01-15T12:00:00Z(HourOfDay)"),
312 				new ParseTest(49, "hourFollowedByPlus", "2011-01-15T12+05", "2011-01-15T12:00:00+05:00(HourOfDay)"),
313 				new ParseTest(50, "hourFollowedByMinus", "2011-01-15T12-05", "2011-01-15T12:00:00-05:00(HourOfDay)"),
314 				new ParseTest(51, "minuteFollowedByZ", "2011-01-15T12:30Z", "2011-01-15T12:30:00Z(MinuteOfHour)"),
315 				new ParseTest(52, "minuteFollowedByPlus", "2011-01-15T12:30+05", "2011-01-15T12:30:00+05:00(MinuteOfHour)"),
316 				new ParseTest(53, "minuteFollowedByMinus", "2011-01-15T12:30-05", "2011-01-15T12:30:00-05:00(MinuteOfHour)"),
317 				new ParseTest(54, "timezoneAfterT", "2011-01T+05:30", "2011-01-01T00:00:00+05:30(MonthOfYear)"),
318 				new ParseTest(55, "dateFollowedByTZ", "2011-01TZ", "2011-01-01T00:00:00Z(MonthOfYear)"),
319 				new ParseTest(56, "dateFollowedByTMinus", "2011-01T-05:30", "2011-01-01T00:00:00-05:30(MonthOfYear)"),
320 				new ParseTest(57, "TFollowedByZ", "TZ", "2000-01-01T00:00:00Z(HourOfDay)"),
321 				new ParseTest(58, "TFollowedByPlus", "T+05", "2000-01-01T00:00:00+05:00(HourOfDay)"),
322 				new ParseTest(59, "TFollowedByMinus", "T-05", "2000-01-01T00:00:00-05:00(HourOfDay)"),
323 
324 				// Fractional separator followed by timezone
325 				new ParseTest(60, "fractionalSeparatorDotFollowedByZ", "2011-01-15T12:30:45.Z", "2011-01-15T12:30:45Z(SecondOfMinute)"),
326 				new ParseTest(61, "fractionalSeparatorCommaFollowedByZ", "2011-01-15T12:30:45,Z", "2011-01-15T12:30:45Z(SecondOfMinute)"),
327 				new ParseTest(62, "fractionalSeparatorDotFollowedByPlus", "2011-01-15T12:30:45.+05:00", "2011-01-15T12:30:45+05:00(SecondOfMinute)"),
328 				new ParseTest(63, "fractionalSeparatorCommaFollowedByPlus", "2011-01-15T12:30:45,+05:00", "2011-01-15T12:30:45+05:00(SecondOfMinute)"),
329 				new ParseTest(64, "fractionalSeparatorDotFollowedByMinus", "2011-01-15T12:30:45.-05:00", "2011-01-15T12:30:45-05:00(SecondOfMinute)"),
330 				new ParseTest(65, "fractionalSeparatorCommaFollowedByMinus", "2011-01-15T12:30:45,-05:00", "2011-01-15T12:30:45-05:00(SecondOfMinute)"),
331 
332 				// Fractional seconds followed by timezone
333 				new ParseTest(66, "fractionalSecondsFollowedByZ", "2011-01-15T12:30:45.123Z", "2011-01-15T12:30:45.123Z(MilliOfSecond)"),
334 				new ParseTest(67, "fractionalSecondsFollowedByPlus", "2011-01-15T12:30:45.123+05:00", "2011-01-15T12:30:45.123+05:00(MilliOfSecond)"),
335 				new ParseTest(68, "fractionalSecondsFollowedByMinus", "2011-01-15T12:30:45.123-05:00", "2011-01-15T12:30:45.123-05:00(MilliOfSecond)"),
336 				new ParseTest(69, "fractionalSecondsCommaFollowedByZ", "2011-01-15T12:30:45,123Z", "2011-01-15T12:30:45.123Z(MilliOfSecond)"),
337 				new ParseTest(70, "fractionalSecondsCommaFollowedByPlus", "2011-01-15T12:30:45,123+05:00", "2011-01-15T12:30:45.123+05:00(MilliOfSecond)"),
338 				new ParseTest(71, "fractionalSecondsCommaFollowedByMinus", "2011-01-15T12:30:45,123-05:00", "2011-01-15T12:30:45.123-05:00(MilliOfSecond)"),
339 
340 				// Nanoseconds (4+ digits) followed by timezone
341 				new ParseTest(72, "nanosecondsFollowedByZ", "2011-01-15T12:30:45.1234Z", "2011-01-15T12:30:45.1234Z(NanoOfSecond)"),
342 				new ParseTest(73, "nanosecondsFollowedByPlus", "2011-01-15T12:30:45.1234+05:00", "2011-01-15T12:30:45.1234+05:00(NanoOfSecond)"),
343 				new ParseTest(74, "nanosecondsFollowedByMinus", "2011-01-15T12:30:45.1234-05:00", "2011-01-15T12:30:45.1234-05:00(NanoOfSecond)"),
344 
345 				// ISO8601 offset range validation (-18:00 ≤ offset ≤ +18:00)
346 				new ParseTest(75, "offsetBoundary_minus18_00", "2011-01-15T12:30:45-18:00", "2011-01-15T12:30:45-18:00(SecondOfMinute)"),
347 				new ParseTest(76, "offsetBoundary_plus18_00", "2011-01-15T12:30:45+18:00", "2011-01-15T12:30:45+18:00(SecondOfMinute)"),
348 				new ParseTest(77, "offsetBoundary_minus18_00_compact", "2011-01-15T12:30:45-1800", "2011-01-15T12:30:45-18:00(SecondOfMinute)"),
349 				new ParseTest(78, "offsetBoundary_plus18_00_compact", "2011-01-15T12:30:45+1800", "2011-01-15T12:30:45+18:00(SecondOfMinute)"),
350 				new ParseTest(79, "offsetBoundary_minus18_hoursOnly", "2011-01-15T12:30:45-18", "2011-01-15T12:30:45-18:00(SecondOfMinute)"),
351 				new ParseTest(80, "offsetBoundary_plus18_hoursOnly", "2011-01-15T12:30:45+18", "2011-01-15T12:30:45+18:00(SecondOfMinute)"),
352 				new ParseTest(81, "offsetValid_withinRange1", "2011-01-15T12:30:45-12:30", "2011-01-15T12:30:45-12:30(SecondOfMinute)"),
353 				new ParseTest(82, "offsetValid_withinRange2", "2011-01-15T12:30:45+12:30", "2011-01-15T12:30:45+12:30(SecondOfMinute)"),
354 				new ParseTest(83, "offsetValid_withinRange3", "2011-01-15T12:30:45-17:59", "2011-01-15T12:30:45-17:59(SecondOfMinute)"),
355 				new ParseTest(84, "offsetValid_withinRange4", "2011-01-15T12:30:45+17:59", "2011-01-15T12:30:45+17:59(SecondOfMinute)"),
356 
357 				// Invalid offset range
358 				new ParseTest(85, "offsetInvalid_belowMinimum", "2011-01-15T12:30:45-19:00", "Invalid ISO8601 timestamp"),
359 				new ParseTest(86, "offsetInvalid_aboveMaximum", "2011-01-15T12:30:45+19:00", "Invalid ISO8601 timestamp"),
360 				new ParseTest(87, "offsetInvalid_belowMinimum_compact", "2011-01-15T12:30:45-1900", "Invalid ISO8601 timestamp"),
361 				new ParseTest(88, "offsetInvalid_aboveMaximum_compact", "2011-01-15T12:30:45+1900", "Invalid ISO8601 timestamp"),
362 				new ParseTest(89, "offsetInvalid_belowMinimum_hoursOnly", "2011-01-15T12:30:45-19", "Invalid ISO8601 timestamp"),
363 				new ParseTest(90, "offsetInvalid_aboveMaximum_hoursOnly", "2011-01-15T12:30:45+19", "Invalid ISO8601 timestamp"),
364 
365 				// Invalid offset format
366 				new ParseTest(91, "offsetInvalid_1digit", "2011-01-15T12:30:45+1", "Invalid ISO8601 timestamp"),
367 				new ParseTest(92, "offsetInvalid_3digits", "2011-01-15T12:30:45+123", "Invalid ISO8601 timestamp"),
368 				new ParseTest(93, "offsetInvalid_5digits", "2011-01-15T12:30:45+12345", "Invalid ISO8601 timestamp"),
369 
370 				// Invalid date/time values
371 				new ParseTest(94, "invalidYearLength", "123", "Invalid ISO8601 timestamp"),
372 				new ParseTest(95, "invalidMonth_00", "2011-00", "Invalid ISO8601 timestamp"),
373 				new ParseTest(96, "invalidMonth_13", "2011-13", "Invalid ISO8601 timestamp"),
374 				new ParseTest(97, "invalidMonth_99", "2011-99", "Invalid ISO8601 timestamp"),
375 				new ParseTest(98, "invalidDay_00", "2011-01-00", "Invalid ISO8601 timestamp"),
376 				new ParseTest(99, "invalidDay_32", "2011-01-32", "Invalid ISO8601 timestamp"),
377 				new ParseTest(100, "invalidDay_99", "2011-01-99", "Invalid ISO8601 timestamp"),
378 				new ParseTest(101, "invalidHour_24", "2011-01-15T24", "Invalid ISO8601 timestamp"),
379 				new ParseTest(102, "invalidHour_99", "2011-01-15T99", "Invalid ISO8601 timestamp"),
380 				new ParseTest(103, "invalidMinute_60", "2011-01-15T12:60", "Invalid ISO8601 timestamp"),
381 				new ParseTest(104, "invalidMinute_99", "2011-01-15T12:99", "Invalid ISO8601 timestamp"),
382 				new ParseTest(105, "invalidSecond_60", "2011-01-15T12:30:60", "Invalid ISO8601 timestamp"),
383 				new ParseTest(106, "invalidSecond_99", "2011-01-15T12:30:99", "Invalid ISO8601 timestamp"),
384 
385 				// Invalid dates for specific months
386 				new ParseTest(107, "invalidDate_Nov31", "2011-11-31", "Invalid date 'NOVEMBER 31'"),
387 				new ParseTest(108, "invalidDate_Feb29_nonLeap", "2011-02-29", "Invalid date 'February 29' as '2011' is not a leap year"),
388 				new ParseTest(109, "invalidDate_Feb30", "2011-02-30", "Invalid date 'FEBRUARY 30'"),
389 				new ParseTest(110, "invalidDate_Apr31", "2011-04-31", "Invalid date 'APRIL 31'"),
390 				new ParseTest(111, "invalidDate_Jun31", "2011-06-31", "Invalid date 'JUNE 31'"),
391 				new ParseTest(112, "invalidDate_Sep31", "2011-09-31", "Invalid date 'SEPTEMBER 31'"),
392 				new ParseTest(113, "validDate_Feb29_leap", "2024-02-29", "2024-02-29T00:00:00Z(DayOfMonth)"),
393 
394 				// Invalid characters in various states
395 				new ParseTest(114, "invalidCharAfterYear", "2011X", "Invalid ISO8601 timestamp"),
396 				new ParseTest(115, "invalidCharAfterYearDash", "2011-X", "Invalid ISO8601 timestamp"),
397 				new ParseTest(116, "invalidCharAfterMonth", "2011-01X", "Invalid ISO8601 timestamp"),
398 				new ParseTest(117, "invalidCharAfterMonthDash", "2011-01-X", "Invalid ISO8601 timestamp"),
399 				new ParseTest(118, "invalidCharAfterDay", "2011-01-15X", "Invalid ISO8601 timestamp"),
400 				new ParseTest(119, "invalidCharAfterT", "TX", "Invalid ISO8601 timestamp"),
401 				new ParseTest(120, "invalidCharAfterHour", "2011-01-15T12X", "Invalid ISO8601 timestamp"),
402 				new ParseTest(121, "invalidCharAfterHourColon", "2011-01-15T12:X", "Invalid ISO8601 timestamp"),
403 				new ParseTest(122, "invalidCharAfterMinute", "2011-01-15T12:30X", "Invalid ISO8601 timestamp"),
404 				new ParseTest(123, "invalidCharAfterMinuteColon", "2011-01-15T12:30:X", "Invalid ISO8601 timestamp"),
405 				new ParseTest(124, "invalidCharAfterSecond", "2011-01-15T12:30:45X", "Invalid ISO8601 timestamp"),
406 				new ParseTest(125, "invalidCharAfterFractionalSeparator", "2011-01-15T12:30:45.X", "Invalid ISO8601 timestamp"),
407 				new ParseTest(126, "invalidCharInFractionalSeconds", "2011-01-15T12:30:45.12X", "Invalid ISO8601 timestamp"),
408 				new ParseTest(127, "invalidCharAfterPlus", "2011-01-15T12:30:45+X", "Invalid ISO8601 timestamp"),
409 				new ParseTest(128, "invalidCharAfterMinus", "2011-01-15T12:30:45-X", "Invalid ISO8601 timestamp"),
410 				new ParseTest(129, "invalidCharInOffsetHours", "2011-01-15T12:30:45+05X", "Invalid ISO8601 timestamp"),
411 				new ParseTest(130, "invalidCharAfterOffsetColon", "2011-01-15T12:30:45+05:X", "Invalid ISO8601 timestamp"),
412 				new ParseTest(131, "invalidCharInOffsetMinutes", "2011-01-15T12:30:45+05:30X", "Invalid ISO8601 timestamp"),
413 
414 				// Invalid character after Z
415 				new ParseTest(132, "invalidCharAfterZ_year", "2011Z5", "Invalid ISO8601 timestamp"),
416 				new ParseTest(133, "invalidCharAfterZ_month", "2011-01ZX", "Invalid ISO8601 timestamp"),
417 				new ParseTest(134, "invalidCharAfterZ_day", "2011-01-15ZX", "Invalid ISO8601 timestamp"),
418 				new ParseTest(135, "invalidCharAfterZ_T", "TZX", "Invalid ISO8601 timestamp"),
419 				new ParseTest(136, "invalidCharAfterZ_hour", "2011-01-15T12ZX", "Invalid ISO8601 timestamp"),
420 				new ParseTest(137, "invalidCharAfterZ_minute", "2011-01-15T12:30ZX", "Invalid ISO8601 timestamp"),
421 				new ParseTest(138, "invalidCharAfterZ_second", "2011-01-15T12:30:45ZX", "Invalid ISO8601 timestamp"),
422 				new ParseTest(139, "invalidCharAfterZ_secondPlus", "2011-01-15T12:30:45Z+", "Invalid ISO8601 timestamp"),
423 				new ParseTest(140, "invalidCharAfterZ_secondMinus", "2011-01-15T12:30:45Z-", "Invalid ISO8601 timestamp"),
424 				new ParseTest(141, "invalidCharAfterZ_secondDigit", "2011-01-15T12:30:45Z5", "Invalid ISO8601 timestamp"),
425 				new ParseTest(142, "invalidCharAfterZ_secondSpace", "2011-01-15T12:30:45Z ", "Invalid ISO8601 timestamp"),
426 				new ParseTest(143, "invalidCharAfterZ_fractionalSeparator", "2011-01-15T12:30:45.ZX", "Invalid ISO8601 timestamp"),
427 				new ParseTest(144, "invalidCharAfterZ_fractionalSeconds", "2011-01-15T12:30:45.123ZX", "Invalid ISO8601 timestamp"),
428 				new ParseTest(145, "invalidCharAfterZ_timeOnly", "T12:30:45ZX", "Invalid ISO8601 timestamp"),
429 
430 				// Invalid nanosecond length (>9 digits)
431 				new ParseTest(146, "invalidNanos_10digits", "2011-01-15T12:30:45.1234567890", "Invalid ISO8601 timestamp"),
432 
433 				// Edge cases with defaultZoneId
434 				new ParseTest(147, "withDefaultZoneId", "2011-01-15T12:30:45", "2011-01-15T12:30:45-05:00(SecondOfMinute)", ZoneId.of("America/New_York")),
435 				new ParseTest(148, "withDefaultZoneId_timeOnly", "T12:30:45", "2000-01-01T12:30:45-05:00(SecondOfMinute)", ZoneId.of("America/New_York")),
436 				new ParseTest(149, "withDefaultZoneId_ignoredWhenZoneInString", "2011-01-15T12:30:45Z", "2011-01-15T12:30:45Z(SecondOfMinute)", ZoneId.of("America/New_York")),
437 
438 				// Invalid inputs (expect exceptions)
439 				new ParseTest(150, "null", null, "Argument 'value' cannot be null."),
440 				new ParseTest(151, "emptyString", "", "Invalid ISO8601 timestamp"),
441 				new ParseTest(152, "invalidString", "invalid-date", "Invalid ISO8601 timestamp"),
442 			};
443 		}
444 
445 		@ParameterizedTest(name = "[{0}] {1}")
446 		@MethodSource("parseTests")
447 		void j01_parse(ParseTest test) {
448 			if (test.defaultZoneId == null) {
449 				testParse(test.expected, test.input);
450 			} else {
451 				testParse(test.expected, test.input, test.defaultZoneId);
452 			}
453 		}
454 
455 		private void testParse(String expected, String in) {
456 			try {
457 				var x = GranularZonedDateTime.of(in, FAKE_TIME_PROVIDER);
458 				assertEquals(expected, x.toString(), "Failed for input: " + in);
459 			} catch (Exception e) {
460 				assertEquals(expected, e.getLocalizedMessage(), "Failed for input: " + in);
461 			}
462 		}
463 
464 		private void testParse(String expected, String in, ZoneId zoneId) {
465 			try {
466 				var x = GranularZonedDateTime.of(in, zoneId, FAKE_TIME_PROVIDER);
467 				assertEquals(expected, x.toString(), "Failed for input: " + in + " with zoneId: " + zoneId);
468 			} catch (Exception e) {
469 				assertEquals(expected, e.getLocalizedMessage(), "Failed for input: " + in + " with zoneId: " + zoneId);
470 			}
471 		}
472 	}
473 }