This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 40f48cbc34 Unit tests
40f48cbc34 is described below

commit 40f48cbc34b58eddc16fbf86815fc79aad19925b
Author: James Bognar <[email protected]>
AuthorDate: Thu Dec 4 07:45:13 2025 -0800

    Unit tests
---
 .../commons/utils/GranularZonedDateTime.java       |  466 ++++++++-
 .../juneau/commons/reflect/ClassInfo_Test.java     |  150 +--
 .../commons/utils/GranularZonedDateTime_Test.java  | 1098 +++++++++++++++++++-
 3 files changed, 1618 insertions(+), 96 deletions(-)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/GranularZonedDateTime.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/GranularZonedDateTime.java
index 2169cecd4c..ccd46759de 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/GranularZonedDateTime.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/GranularZonedDateTime.java
@@ -16,6 +16,7 @@
  */
 package org.apache.juneau.commons.utils;
 
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
 import static org.apache.juneau.commons.utils.StateEnum.*;
 import static org.apache.juneau.commons.utils.StringUtils.*;
 import static org.apache.juneau.commons.utils.ThrowableUtils.*;
@@ -67,16 +68,7 @@ public class GranularZonedDateTime {
         * @throws BasicRuntimeException If the string cannot be parsed as a 
valid timestamp.
         */
        public static GranularZonedDateTime parse(String seg) {
-               //seg = seg.replace(' ', 'T').replace(',', '.');
-               //var precision = getPrecisionFromString(seg);
-               ZonedDateTime zdt = fromIso8601(seg);
-               if (nn(zdt)) {
-                       // Determine precision based on the input string
-                       var precision = getPrecisionFromString(seg);
-                       return new GranularZonedDateTime(zdt, precision);
-               }
-
-               throw rex("Invalid date encountered: ''{0}''", seg);
+               return parse2(seg);
        }
 
        /** The ZonedDateTime value */
@@ -566,4 +558,458 @@ public class GranularZonedDateTime {
                return ZonedDateTime.parse(validDate, 
DateTimeFormatter.ISO_DATE_TIME);
        }
 
+       public static GranularZonedDateTime parse2(String seg) {
+               return parse2(seg, null);
+       }
+
+       public static GranularZonedDateTime parse2(String seg, ZoneId 
defaultZoneId) {
+               assertArgNotNull("seg", seg);
+               var digit = StringUtils.DIGIT;
+
+               // States:
+               // S01: Looking for Y(S02) or T(S07).
+               // S02: Found Y, looking for Y(S02)/-(S03)/T(S07).
+               // S03: Found -, looking for M(S04).
+               // S04: Found M, looking for M(S04)/-(S05)/T(S07).
+               // S05: Found -, looking for D(S10).
+               // S06  Found D, looking for D(S06)/T(S07).
+               // S07: Found T, looking for h(S08)/Z(S15)/+(S16)/-(S17).
+               // S08: Found h, looking for h(S08)/:(S09)/Z(S15)/+(S16)/-(S17).
+               // S09: Found :, looking for m(S10).
+               // S10: Found m, looking for m(S10)/:(S11)/Z(S15)/+(S16)/-(S17).
+               // S11: Found :, looking for s(S12).
+               // S12: Found s, looking for s(S12)/.(S13)/Z(S15)/+(S16)/-(S17).
+               // S13: Found ., looking for S(S14)/Z(S15)/+(S16)/-(S17).
+               // S14: Found S, looking for S(S14)/Z(S15)/+(S16)/-(S17).
+               // S15: Found Z.
+               // S16: Found +, looking for oh(S18).
+               // S17: Found -, looking for oh(S18).
+               // S18: Found oh, looking for oh(S18)/:(S19).
+               // S19: Found :, looking for om(S20).
+               // S20: Found om, looking for om(S20).
+
+
+               int year = 1, month = 1, day = 1, hour = 0, minute = 0, second 
= 0, nanos = 0, ohour = -1, ominute = -1;
+               boolean nego = false; // negative offset
+               boolean timeOnly = false; // Track if format started with "T" 
(time-only)
+               ZoneId zoneId = null;
+               var state = S1;
+               var mark = 0;
+               ChronoField precision = ChronoField.YEAR; // Track precision as 
we go
+
+               for (var i = 0; i < seg.length(); i++) {
+                       var c = seg.charAt(i);
+
+                       if (state == S1) {
+                               // S01: Looking for Y(S02) or T(S07)
+                               if (digit.contains(c)) {
+                                       mark = i;
+                                       state = S2;
+                               } else if (c == 'T') {
+                                       timeOnly = true; // Mark as time-only 
format
+                                       state = S7;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S2) {
+                               // S02: Found Y, looking for 
Y(S02)/-(S03)/T(S07)
+                               if (digit.contains(c)) {
+                                       // Stay in S2
+                               } else if (c == '-') {
+                                       year = parse(seg, 4, mark, i, 0, 9999);
+                                       state = S3;
+                               } else if (c == 'T') {
+                                       year = parse(seg, 4, mark, i, 0, 9999);
+                                       state = S7;
+                               } else if (c == 'Z') {
+                                       zoneId = ZoneId.of("Z");
+                                       year = parse(seg, 4, mark, i, 0, 9999);
+                                       state = S15;
+                               } else if (c == '+') {
+                                       year = parse(seg, 4, mark, i, 0, 9999);
+                                       nego = false;
+                                       state = S16;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S3) {
+                               // S03: Found -, looking for M(S04)
+                               if (digit.contains(c)) {
+                                       mark = i;
+                                       state = S4;
+                                       precision = ChronoField.MONTH_OF_YEAR;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S4) {
+                               // S04: Found M, looking for 
M(S04)/-(S05)/T(S07)
+                               if (digit.contains(c)) {
+                                       // Stay in S4
+                               } else if (c == '-') {
+                                       month = parse(seg, 2, mark, i, 1, 12);
+                                       state = S5;
+                               } else if (c == 'T') {
+                                       month = parse(seg, 2, mark, i, 1, 12);
+                                       state = S7;
+                               } else if (c == 'Z') {
+                                       month = parse(seg, 2, mark, i, 1, 12);
+                                       zoneId = ZoneId.of("Z");
+                                       state = S15;
+                               } else if (c == '+') {
+                                       month = parse(seg, 2, mark, i, 1, 12);
+                                       nego = false;
+                                       state = S16;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S5) {
+                               // S05: Found -, looking for D(S06)
+                               if (digit.contains(c)) {
+                                       mark = i;
+                                       state = S6;
+                                       precision = ChronoField.DAY_OF_MONTH;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S6) {
+                               // S06: Found D, looking for D(S06)/T(S07)
+                               if (digit.contains(c)) {
+                                       // Stay in S6
+                               } else if (c == 'T') {
+                                       day = parse(seg, 2, mark, i, 1, 31);
+                                       state = S7;
+                               } else if (c == 'Z') {
+                                       day = parse(seg, 2, mark, i, 1, 31);
+                                       zoneId = ZoneId.of("Z");
+                                       state = S15;
+                               } else if (c == '+') {
+                                       day = parse(seg, 2, mark, i, 1, 31);
+                                       nego = false;
+                                       state = S16;
+                               } else if (c == '-') {
+                                       day = parse(seg, 2, mark, i, 1, 31);
+                                       nego = true;
+                                       state = S17;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S7) {
+                               // S07: Found T, looking for 
h(S08)/Z(S15)/+(S16)/-(S17)
+                               if (digit.contains(c)) {
+                                       mark = i;
+                                       state = S8;
+                                       precision = ChronoField.HOUR_OF_DAY;
+                               } else if (c == 'Z') {
+                                       zoneId = ZoneId.of("Z");
+                                       state = S15;
+                               } else if (c == '+') {
+                                       nego = false;
+                                       state = S16;
+                               } else if (c == '-') {
+                                       nego = true;
+                                       state = S17;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S8) {
+                               // S08: Found h, looking for 
h(S08)/:(S09)/Z(S15)/+(S16)/-(S17)
+                               if (digit.contains(c)) {
+                                       // Stay in S8
+                               } else if (c == ':') {
+                                       hour = parse(seg, 2, mark, i, 0, 23);
+                                       state = S9;
+                               } else if (c == 'Z') {
+                                       hour = parse(seg, 2, mark, i, 0, 23);
+                                       zoneId = ZoneId.of("Z");
+                                       state = S15;
+                               } else if (c == '+') {
+                                       hour = parse(seg, 2, mark, i, 0, 23);
+                                       nego = false;
+                                       state = S16;
+                               } else if (c == '-') {
+                                       hour = parse(seg, 2, mark, i, 0, 23);
+                                       nego = true;
+                                       state = S17;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S9) {
+                               // S09: Found :, looking for m(S10)
+                               if (digit.contains(c)) {
+                                       mark = i;
+                                       state = S10;
+                                       precision = ChronoField.MINUTE_OF_HOUR;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S10) {
+                               // S10: Found m, looking for 
m(S10)/:(S11)/Z(S15)/+(S16)/-(S17)
+                               if (digit.contains(c)) {
+                                       // Stay in S10
+                               } else if (c == ':') {
+                                       minute = parse(seg, 2, mark, i, 0, 59);
+                                       state = S11;
+                               } else if (c == 'Z') {
+                                       minute = parse(seg, 2, mark, i, 0, 59);
+                                       zoneId = ZoneId.of("Z");
+                                       state = S15;
+                               } else if (c == '+') {
+                                       minute = parse(seg, 2, mark, i, 0, 59);
+                                       nego = false;
+                                       state = S16;
+                               } else if (c == '-') {
+                                       minute = parse(seg, 2, mark, i, 0, 59);
+                                       nego = true;
+                                       state = S17;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S11) {
+                               // S11: Found :, looking for s(S12)
+                               if (digit.contains(c)) {
+                                       mark = i;
+                                       state = S12;
+                                       precision = 
ChronoField.SECOND_OF_MINUTE;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S12) {
+                               // S12: Found s, looking for 
s(S12)/.(S13)/Z(S15)/+(S16)/-(S17)
+                               if (digit.contains(c)) {
+                                       // Stay in S12
+                       } else if (c == '.' || c == ',') {
+                               second = parse(seg, 2, mark, i, 0, 59);
+                               state = S13;
+                               // Precision will be set based on number of 
fractional digits
+                               } else if (c == 'Z') {
+                                       second = parse(seg, 2, mark, i, 0, 59);
+                                       zoneId = ZoneId.of("Z");
+                                       state = S15;
+                               } else if (c == '+') {
+                                       second = parse(seg, 2, mark, i, 0, 59);
+                                       nego = false;
+                                       state = S16;
+                               } else if (c == '-') {
+                                       second = parse(seg, 2, mark, i, 0, 59);
+                                       nego = true;
+                                       state = S17;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S13) {
+                               // S13: Found . or ,, looking for 
S(S14)/Z(S15)/+(S16)/-(S17)
+                               if (digit.contains(c)) {
+                                       mark = i;
+                                       state = S14;
+                               } else if (c == 'Z') {
+                                       zoneId = ZoneId.of("Z");
+                                       state = S15;
+                               } else if (c == '+') {
+                                       nego = false;
+                                       state = S16;
+                               } else if (c == '-') {
+                                       nego = true;
+                                       state = S17;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S14) {
+                               // S14: Found S, looking for 
S(S14)/Z(S15)/+(S16)/-(S17)
+                               if (digit.contains(c)) {
+                                       // Stay in S14
+                               } else if (c == 'Z') {
+                                       nanos = parseNanos(seg, mark, i);
+                                       zoneId = ZoneId.of("Z");
+                                       // Set precision based on number of 
fractional digits: 1-3 = milliseconds, 4-9 = nanoseconds
+                                       var digitCount = i - mark;
+                                       precision = (digitCount <= 3) ? 
ChronoField.MILLI_OF_SECOND : ChronoField.NANO_OF_SECOND;
+                                       state = S15;
+                               } else if (c == '+') {
+                                       nanos = parseNanos(seg, mark, i);
+                                       // Set precision based on number of 
fractional digits: 1-3 = milliseconds, 4-9 = nanoseconds
+                                       var digitCount = i - mark;
+                                       precision = (digitCount <= 3) ? 
ChronoField.MILLI_OF_SECOND : ChronoField.NANO_OF_SECOND;
+                                       nego = false;
+                                       state = S16;
+                               } else if (c == '-') {
+                                       nanos = parseNanos(seg, mark, i);
+                                       // Set precision based on number of 
fractional digits: 1-3 = milliseconds, 4-9 = nanoseconds
+                                       var digitCount = i - mark;
+                                       precision = (digitCount <= 3) ? 
ChronoField.MILLI_OF_SECOND : ChronoField.NANO_OF_SECOND;
+                                       nego = true;
+                                       state = S17;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S15) {
+                               // Shouldn't find anything after Z
+                               throw bad(seg, i);
+                       } else if (state == S16) {
+                               // S16: Found +, looking for oh(S18)
+                               if (digit.contains(c)) {
+                                       mark = i;
+                                       state = S18;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S17) {
+                               // S17: Found -, looking for oh(S18)
+                               if (digit.contains(c)) {
+                                       mark = i;
+                                       state = S18;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else if (state == S18) {
+                               // S18: Found oh, looking for oh(S18)/:(S19)/end
+                               if (digit.contains(c)) {
+                                       // Stay in S18
+                               } else if (c == ':') {
+                                       ohour = parse(seg, 2, mark, i, 0, 18);
+                                       state = S19;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                               // If we reach end of string, ohour is complete 
(2 digits)
+                       } else if (state == S19) {
+                               // S19: Found :, looking for om(S20)
+                               if (digit.contains(c)) {
+                                       mark = i;
+                                       state = S20;
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       } else /* (state == S20) */ {
+                               // S20: Found om, looking for om(S20)
+                               if (digit.contains(c)) {
+                                       // Stay in S20
+                               } else {
+                                       throw bad(seg, i);
+                               }
+                       }
+               }
+
+               var end = seg.length(); // end is exclusive (one past last 
character)
+               if (state.isAny(S1, S3, S5, S7, S9, S11, S13, S16, S17, S19)) {
+                       throw bad(seg, end - 1);
+               } else if (state == S2) {
+                       // S02: Found Y, looking for Y(S02)/-(S03)/T(S07).
+                       year = parse(seg, 4, mark, end, 0, 9999);
+                       precision = ChronoField.YEAR;
+               } else if (state == S4) {
+                       // S04: Found M, looking for M(S04)/-(S05)/T(S07).
+                       month = parse(seg, 2, mark, end, 1, 12);
+                       precision = ChronoField.MONTH_OF_YEAR;
+               } else if (state == S6) {
+                       // S06  Found D, looking for D(S06)/T(S07).
+                       day = parse(seg, 2, mark, end, 1, 31);
+                       precision = ChronoField.DAY_OF_MONTH;
+               } else if (state == S8) {
+                       // S08: Found h, looking for 
h(S08)/:(S09)/Z(S15)/+(S16)/-(S17).
+                       hour = parse(seg, 2, mark, end, 0, 23);
+                       precision = ChronoField.HOUR_OF_DAY;
+               } else if (state == S10) {
+                       // S10: Found m, looking for 
m(S10)/:(S11)/Z(S15)/+(S16)/-(S17).
+                       minute = parse(seg, 2, mark, end, 0, 59);
+                       precision = ChronoField.MINUTE_OF_HOUR;
+               } else if (state == S12) {
+                       // S12: Found s, looking for 
s(S12)/.(S13)/Z(S15)/+(S16)/-(S17).
+                       second = parse(seg, 2, mark, end, 0, 59);
+                       precision = ChronoField.SECOND_OF_MINUTE;
+               } else if (state == S14) {
+                       // S14: Found S, looking for 
S(S14)/Z(S15)/+(S16)/-(S17).
+                       nanos = parseNanos(seg, mark, end);
+                       // Set precision based on number of digits: 1-3 = 
milliseconds, 4-9 = nanoseconds
+                       var digitCount = end - mark;
+                       precision = (digitCount <= 3) ? 
ChronoField.MILLI_OF_SECOND : ChronoField.NANO_OF_SECOND;
+               } else if (state == S15) {
+                       // S15: Found Z.
+               } else if (state == S18) {
+                       // S18: Found oh, looking for oh(S18)/:(S19).
+                       // Check if we have 2 digits (+hh) or 4 digits (+hhmm)
+                       if (end - mark == 2) {
+                               ohour = parse(seg, 2, mark, end, 0, 18);
+                       } else if (end - mark == 4) {
+                               // +hhmm format: parse hours from mark to 
mark+2, minutes from mark+2 to end
+                               ohour = parse(seg, 2, mark, mark + 2, 0, 18);
+                               ominute = parse(seg, 2, mark + 2, end, 0, 59);
+                       } else {
+                               throw bad(seg, mark);
+                       }
+               } else /* (state == S20) */ {
+                       // S20: Found om, looking for om(S20).
+                       ominute = parse(seg, 2, mark, end, 0, 59);
+               }
+
+               // Build ZoneId if we have offset information
+               if (zoneId == null) {
+                       if (ohour >= 0) {
+                               if (ominute >= 0) {
+                                       // If negative offset, both hours and 
minutes must be negative
+                                       var offset = 
ZoneOffset.ofHoursMinutes(nego ? -ohour : ohour, nego ? -ominute : ominute);
+                                       zoneId = offset;
+                               } else {
+                                       var offset = ZoneOffset.ofHours(nego ? 
-ohour : ohour);
+                                       zoneId = offset;
+                               }
+                       }
+               }
+
+               // Use provided default zone if no zone specified, otherwise 
use system default
+               if (zoneId == null) {
+                       zoneId = defaultZoneId != null ? defaultZoneId : 
ZoneId.systemDefault();
+               }
+
+               // Construct ZonedDateTime from parsed values
+               // Default values for missing components
+               // For time-only formats (started with "T"), use current date
+               // For date formats, default to 1/1/1
+               if (timeOnly) {
+                       // Time-only format: use current year/month/day
+                       var now = ZonedDateTime.now(zoneId);
+                       year = now.getYear();
+                       month = now.getMonthValue();
+                       day = now.getDayOfMonth();
+               }
+
+               var localDateTime = LocalDateTime.of(year, month, day, hour, 
minute, second);
+               if (nanos > 0) {
+                       localDateTime = localDateTime.plusNanos(nanos);
+               }
+
+               var zdt = ZonedDateTime.of(localDateTime, zoneId);
+
+               // Return GranularZonedDateTime with the determined precision
+               return new GranularZonedDateTime(zdt, precision);
+       }
+
+       private static DateTimeParseException bad(String s, int pos) {
+               return new DateTimeParseException("Invalid ISO8601 timestamp", 
s, pos);
+       }
+
+       private static int parse(String s, int chars, int pos, int end, int 
min, int max) {
+               if (end-pos != chars) throw bad(s, pos);
+               var i = Integer.parseInt(s, pos, end, 10);
+               if (i < min || i > max) throw bad(s, pos);
+               return i;
+       }
+
+       private static int parseNanos(String s, int pos, int end) {
+               var len = end - pos; // Length of the substring being parsed
+               if (len > 9) {
+                       throw bad(s, pos);
+               }
+               var n = Integer.parseInt(s, pos, end, 10);
+               // Convert to nanoseconds based on number of digits
+               // 1 digit = hundreds of milliseconds, 2 = tens, 3 = 
milliseconds, etc.
+               if (len == 1) return n * 100000000;
+               if (len == 2) return n * 10000000;
+               if (len == 3) return n * 1000000;
+               if (len == 4) return n * 100000;
+               if (len == 5) return n * 10000;
+               if (len == 6) return n * 1000;
+               if (len == 7) return n * 100;
+               if (len == 8) return n * 10;
+               return n;
+       }
 }
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ClassInfo_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ClassInfo_Test.java
index 6b04833296..234fcdf61a 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ClassInfo_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ClassInfo_Test.java
@@ -558,13 +558,13 @@ public class ClassInfo_Test extends TestBase {
                // To test the at == null branch, we need inner != null but 
inner.arrayType() == null
                // This is difficult to achieve with normal Java classes, but 
we can verify the non-null branch works
                // The non-null branch (at != null) is already covered above 
with String.class and String[].class
-
+               
                // Verify that the non-null branch works correctly (at != null 
-> return of(at))
                var ci4 = ClassInfo.of(Integer.class);
                var arrayType3 = ci4.arrayType();
                assertNotNull(arrayType3);
                assertEquals(Integer[].class, arrayType3.inner());
-
+               
                // Note: The at == null branch at line 391 is a defensive check 
that may be unreachable
                // in practice with normal Java classes, but the code includes 
it for safety
        }
@@ -617,7 +617,7 @@ public class ClassInfo_Test extends TestBase {
                
check("CI2.i2a(),CI2.i2b(),CI1.i1a(),CI1.i1b(),CC1.c1a(),CC1.c1b(),CC1.i1a(),CC2.c2a(),CC2.c2b(),CC2.i1b(),CC2.i2a(),CC2.i2b(),CC3.c3a(),CC3.c3b(),CC3.i2b()",
 cc3.getAllMethodsTopDown());
                // Test twice to verify caching
                
check("CI2.i2a(),CI2.i2b(),CI1.i1a(),CI1.i1b(),CC1.c1a(),CC1.c1b(),CC1.i1a(),CC2.c2a(),CC2.c2b(),CC2.i1b(),CC2.i2a(),CC2.i2b(),CC3.c3a(),CC3.c3b(),CC3.i2b()",
 cc3.getAllMethodsTopDown());
-
+               
                // Test line 248/590/615: getAllMethodsTopDown() initialization 
and method call
                // Test with class that has no methods
                var objectCi = ClassInfo.of(Object.class);
@@ -626,7 +626,7 @@ public class ClassInfo_Test extends TestBase {
                // Object class has methods, but we filter out Object.class 
methods in publicMethods
                // getAllMethodsTopDown includes all methods from all parents, 
so it should have methods
                // Actually, getAllMethodsTopDown uses getAllParents() which 
includes Object, so it should have methods
-
+               
                // Test with interface that has no methods
                var emptyInterface = ClassInfo.of(EmptyInterface.class);
                var emptyMethods = emptyInterface.getAllMethodsTopDown();
@@ -634,7 +634,7 @@ public class ClassInfo_Test extends TestBase {
                // Empty interface should have no methods
                assertTrue(emptyMethods.isEmpty() || emptyMethods.size() >= 0); 
// May have Object methods
        }
-
+       
        // Helper interface for testing
        interface EmptyInterface {}
 
@@ -775,12 +775,12 @@ public class ClassInfo_Test extends TestBase {
                // Number can accept int (parent type)
                assertTrue(ClassInfo.of(Number.class).canAcceptArg(42));
                
assertTrue(ClassInfo.of(Number.class).canAcceptArg(Integer.valueOf(42)));
-
+               
                // Test line 434: all 4 branches of if (this.isPrimitive() || 
child.getClass().isPrimitive())
                // Branch 1: this.isPrimitive() == true && 
child.getClass().isPrimitive() == true
                // Note: When you pass a primitive value like 42, it gets 
autoboxed to Integer, so child.getClass() returns Integer.class (not primitive)
                // This branch is likely unreachable in practice since 
primitive values are always autoboxed
-
+               
                // Branch 2: this.isPrimitive() == true && 
child.getClass().isPrimitive() == false (already covered above at line 707)
                // Branch 3: this.isPrimitive() == false && 
child.getClass().isPrimitive() == true (already covered above at line 710)
                // Branch 4: this.isPrimitive() == false && 
child.getClass().isPrimitive() == false
@@ -788,7 +788,7 @@ public class ClassInfo_Test extends TestBase {
                // This is the missing branch - need to test when condition is 
false (both false), so we skip the if and return false
                assertFalse(ClassInfo.of(String.class).canAcceptArg(new 
Object())); // Both non-primitive, isInstance false, condition false, returns 
false
                assertFalse(ClassInfo.of(String.class).canAcceptArg(42)); // 
String is not primitive, Integer is not primitive, isInstance false, condition 
false, returns false
-
+               
                // Test case where this is primitive but child is not (branch 
2) - different primitive types that don't match
                
assertFalse(ClassInfo.of(int.class).canAcceptArg(Long.valueOf(42L))); // int is 
primitive, Long is not primitive, different types, condition true, returns 
false from isParentOf
        }
@@ -838,7 +838,7 @@ public class ClassInfo_Test extends TestBase {
                // For types without inner class, should return null (line 
474-475)
                var ci = ClassInfo.of((Class<?>)null, pType);
                assertNull(ci.componentType());
-
+               
                // Test line 476-477: componentType() method
                // For non-array types, inner.componentType() returns null, so 
ct == null branch is taken
                var nonArray = ClassInfo.of(String.class);
@@ -846,7 +846,7 @@ public class ClassInfo_Test extends TestBase {
                // String.class.componentType() returns null for non-array types
                // So the ternary at line 477: ct == null ? null : of(ct) takes 
the null branch
                assertNull(ct);
-
+               
                // For array types, inner.componentType() returns non-null, so 
ct != null branch is taken
                var array = ClassInfo.of(String[].class);
                var ct2 = array.componentType();
@@ -907,7 +907,7 @@ public class ClassInfo_Test extends TestBase {
                // G4 has no declared annotations (inherits from G3)
                var declared2 = g4.getDeclaredAnnotations();
                assertTrue(declared2.isEmpty());
-
+               
                // Test with null inner (line 231: opt(inner) returns empty 
when inner is null)
                var ci = ClassInfo.of((Class<?>)null, pType);
                var declared3 = ci.getDeclaredAnnotations();
@@ -990,7 +990,7 @@ public class ClassInfo_Test extends TestBase {
                check("", pTypeDimensionalInfo.getDeclaredInterfaces());
                check("Map", pTypeGenericInfo.getDeclaredInterfaces());
                check("", pTypeGenericArgInfo.getDeclaredInterfaces());
-
+               
                // Test line 190/235: inner == null case
                // When inner is null, opt(inner) returns empty, so 
orElse(liste()) returns empty list
                var ci = ClassInfo.of((Class<?>)null, pType);
@@ -1141,7 +1141,7 @@ public class ClassInfo_Test extends TestBase {
                var local = ClassInfo.of(LocalClass.class);
                var constructor = local.getEnclosingConstructor();
                assertNull(constructor);
-
+               
                // Test line 952: ec != null branch - class declared inside 
constructor
                // Create an instance to trigger the constructor and capture 
the local class
                new EnclosingConstructorTestClass();
@@ -1306,7 +1306,7 @@ public class ClassInfo_Test extends TestBase {
                assertNull(pTypeInfo.getNameCanonical());
                assertNull(pTypeDimensionalInfo.getNameCanonical());
                assertNull(pTypeGenericInfo.getNameCanonical());
-
+               
                // Test line 1112: inner == null && ! isParameterizedType 
(branch 3)
                // Create a ClassInfo with inner == null and innerType being a 
TypeVariable (not ParameterizedType)
                // aType is a TypeVariable from A2 extends Value<A1>
@@ -1421,22 +1421,22 @@ public class ClassInfo_Test extends TestBase {
                assertNotNull(formatted2);
                // When inner is null but isParameterizedType is true, code 
extracts raw type and uses its simple name
                assertEquals("Map", formatted2);
-
+               
                // Test line 326: FULL format separator replacement (4 branches)
                // Branch 1: separator != '$' && sb.indexOf("$") != -1 (true, 
true) - should replace '$' with separator
                var ci12 = ClassInfo.of(Map.Entry.class);
                assertEquals("java.util.Map.Entry", ci12.getNameFormatted(FULL, 
false, '.', BRACKETS));
-
+               
                // Branch 2: separator != '$' && sb.indexOf("$") == -1 (true, 
false) - no '$' in name, no replacement needed
                var ci13 = ClassInfo.of(String.class);
                assertEquals("java.lang.String", ci13.getNameFormatted(FULL, 
false, '.', BRACKETS));
-
+               
                // Branch 3: separator == '$' && sb.indexOf("$") != -1 (false, 
true) - separator is '$', no replacement
                assertEquals("java.util.Map$Entry", ci12.getNameFormatted(FULL, 
false, '$', BRACKETS));
-
+               
                // Branch 4: separator == '$' && sb.indexOf("$") == -1 (false, 
false) - separator is '$', no '$' in name
                assertEquals("java.lang.String", ci13.getNameFormatted(FULL, 
false, '$', BRACKETS));
-
+               
                // Test line 360: SIMPLE format with null class (not 
ParameterizedType) - should use innerType.getTypeName()
                // Use an existing TypeVariable from MC class which has type 
parameters
                var typeVar = MC.class.getTypeParameters()[0]; // MC<K,E> has K 
as first type parameter
@@ -1656,16 +1656,16 @@ public class ClassInfo_Test extends TestBase {
                check("java.util", pTypeDimensionalInfo.getPackage());
                check("java.util", pTypeGenericInfo.getPackage());
                check(null, pTypeGenericArgInfo.getPackage());
-
+               
                // Test line 229: packageInfo memoization with null inner
                // When inner is null, opt(inner) is empty, so should return 
null
                var ci = ClassInfo.of((Class<?>)null, pType);
                assertNull(ci.getPackage());
-
+               
                // Test with primitive types (getPackage() returns null)
                var intCi = ClassInfo.of(int.class);
                assertNull(intCi.getPackage()); // Primitives have no package
-
+               
                // Test with arrays of primitives (getPackage() returns null)
                var intArrayCi = ClassInfo.of(int[].class);
                assertNull(intArrayCi.getPackage()); // Arrays of primitives 
have no package
@@ -1741,7 +1741,7 @@ public class ClassInfo_Test extends TestBase {
 
                // Nested type
                check("MM", mn.getParameterType(1, HashMap.class));
-
+               
                // Note: Line 1375 (actualType3 = 
(TypeVariable<?>)entry.getValue()) is executed when
                // resolving a type variable in a nested inner class where the 
value in the outer type map
                // is itself a TypeVariable (not a Class). This requires a 
complex nested generic scenario
@@ -1795,13 +1795,13 @@ public class ClassInfo_Test extends TestBase {
        // Only compile if sealed classes are available
        public static sealed class SealedTestClass permits SealedSubclass1, 
SealedSubclass2 {
        }
-
+       
        public static final class SealedSubclass1 extends SealedTestClass {
        }
-
+       
        public static final class SealedSubclass2 extends SealedTestClass {
        }
-
+       
        // Record class for testing isRecord (Java 14+)
        // Only compile if records are available
        public static record TestRecord(String name, int value) {
@@ -1822,7 +1822,7 @@ public class ClassInfo_Test extends TestBase {
                var empty = ci.getPermittedSubclasses();
                assertNotNull(empty);
                assertTrue(empty.isEmpty());
-
+               
                // Test line 1465: inner != null && inner.isSealed() - sealed 
class with permitted subclasses
                // Only test if sealed classes are available (Java 17+)
                try {
@@ -1942,7 +1942,7 @@ public class ClassInfo_Test extends TestBase {
                // Test on types
                check("", aTypeInfo.getPublicFields());
                check("", pTypeGenericArgInfo.getPublicFields());
-
+               
                // Test line 249: publicFields memoization
                // This line uses parents.get().stream() which requires inner 
to be non-null
                // When inner is null, parents.get() will return empty list, so 
publicFields should be empty
@@ -2028,12 +2028,12 @@ public class ClassInfo_Test extends TestBase {
                        // Records not available, skip test
                        assertTrue(cc3.getRecordComponents().isEmpty());
                }
-
+               
                // Test line 240: recordComponents memoization
                // When inner is null, opt(inner) is empty, so should return 
empty list
                var ci = ClassInfo.of((Class<?>)null, pType);
                assertTrue(ci.getRecordComponents().isEmpty());
-
+               
                // When inner is not null but isRecord() is false, filter 
should return empty, so should return empty list
                assertTrue(aClass.getRecordComponents().isEmpty());
        }
@@ -2048,11 +2048,11 @@ public class ClassInfo_Test extends TestBase {
                var method = repeatable.getRepeatedAnnotationMethod();
                // @Repeatable itself is not repeatable, so should return null
                assertNull(method);
-
+               
                // Test isRepeatedAnnotation() (line 2135)
                // When getRepeatedAnnotationMethod() returns null, 
isRepeatedAnnotation() should return false
                assertFalse(repeatable.isRepeatedAnnotation());
-
+               
                // Test with a class that has a repeatable annotation method
                // TestRepeatableContainer is the container annotation for 
TestRepeatable
                // It has a value() method that returns TestRepeatable[], and 
TestRepeatable is marked with @Repeatable(TestRepeatableContainer.class)
@@ -2060,23 +2060,23 @@ public class ClassInfo_Test extends TestBase {
                var containerMethod = container.getRepeatedAnnotationMethod();
                assertNotNull(containerMethod);  // Should find the value() 
method
                assertTrue(container.isRepeatedAnnotation());  // Line 2135: 
getRepeatedAnnotationMethod() != null returns true
-
+               
                // Test line 2364 branches: return r != null && 
r.value().equals(inner);
                // Branch 1: r != null is false (r is null) - when component 
type doesn't have @Repeatable
                // This is covered by NonRepeatableArrayContainer which has 
value() returning String[], but String is not a repeatable annotation
                var nonRepeatableContainer = 
ClassInfo.of(NonRepeatableArrayContainer.class);
                
assertNull(nonRepeatableContainer.getRepeatedAnnotationMethod());  // Should 
return null because String is not repeatable
-
+               
                // Branch 2: r != null is true, r.value().equals(inner) is true 
- covered by TestRepeatableContainer above
                // TestRepeatableContainer has value() returning 
TestRepeatable[], and TestRepeatable is marked with 
@Repeatable(TestRepeatableContainer.class)
                // So when checking TestRepeatableContainer, r.value() equals 
TestRepeatableContainer.class (inner)
-
+               
                // Branch 3: r != null is true, r.value().equals(inner) is 
false - when @Repeatable points to a different container
                // WrongContainer has value() returning TestRepeatable[], but 
TestRepeatable's @Repeatable points to TestRepeatableContainer, not 
WrongContainer
                // So when checking WrongContainer, r.value() would be 
TestRepeatableContainer.class, not WrongContainer.class, so equals(inner) is 
false
                var wrongContainer = ClassInfo.of(WrongContainer.class);
                assertNull(wrongContainer.getRepeatedAnnotationMethod());  // 
Should return null because the @Repeatable points to a different container
-
+               
                // Test that non-repeatable classes return false
                assertFalse(aClass.isRepeatedAnnotation());
        }
@@ -2090,12 +2090,12 @@ public class ClassInfo_Test extends TestBase {
                assertNotNull(signers);
                // Most classes won't have signers unless from a signed JAR
                assertTrue(signers.isEmpty() || !signers.isEmpty());
-
+               
                // Test line 244: signers memoization
                // When inner is null, opt(inner) is empty, so should return 
empty list
                var ci = ClassInfo.of((Class<?>)null, pType);
                assertTrue(ci.getSigners().isEmpty());
-
+               
                // When inner is not null but getSigners() returns null, map 
should handle null and return empty list
                // Most classes return null from getSigners(), which is then 
wrapped in u(l(x)) to create empty list
                // This is already tested above with aClass.getSigners()
@@ -2251,18 +2251,18 @@ public class ClassInfo_Test extends TestBase {
                assertTrue(aClass.is(NOT_RECORD));
                assertTrue(aClass.is(NOT_SEALED));
                assertTrue(aClass.is(NOT_SYNTHETIC));
-
+               
                // Test positive ElementFlag cases (lines 1772, 1774, 1775, 
1776, 1781, 1783, 1787, 1789, 1791, 1793)
                // ANNOTATION (line 1772)
                assertTrue(ClassInfo.of(A.class).is(ANNOTATION));
                assertFalse(aClass.is(ANNOTATION));
-
+               
                // NOT_ANNOTATION (line 1773) - test both branches
                // Branch 1: isAnnotation() returns false, so NOT_ANNOTATION 
returns true
                assertTrue(aClass.is(NOT_ANNOTATION));
                // Branch 2: isAnnotation() returns true, so NOT_ANNOTATION 
returns false
                assertFalse(ClassInfo.of(A.class).is(NOT_ANNOTATION));
-
+               
                // ANONYMOUS and NOT_ANONYMOUS (lines 1774, 1775)
                // Anonymous classes are created dynamically, so we test 
NOT_ANONYMOUS
                assertTrue(aClass.is(NOT_ANONYMOUS));
@@ -2273,27 +2273,27 @@ public class ClassInfo_Test extends TestBase {
                        assertTrue(anonymousInfo.is(ANONYMOUS));
                        assertFalse(anonymousInfo.is(NOT_ANONYMOUS));
                }
-
+               
                // ARRAY (line 1776)
                assertTrue(ClassInfo.of(String[].class).is(ARRAY));
                assertFalse(aClass.is(ARRAY));
-
+               
                // NOT_ARRAY (line 1777) - test both branches
                // Branch 1: isArray() returns false, so NOT_ARRAY returns true
                assertTrue(aClass.is(NOT_ARRAY));
                // Branch 2: isArray() returns true, so NOT_ARRAY returns false
                assertFalse(ClassInfo.of(String[].class).is(NOT_ARRAY));
-
+               
                // ENUM (line 1781)
                assertTrue(ClassInfo.of(ClassArrayFormat.class).is(ENUM));
                assertFalse(aClass.is(ENUM));
-
+               
                // NOT_ENUM (line 1782) - test both branches
                // Branch 1: isEnum() returns false, so NOT_ENUM returns true
                assertTrue(aClass.is(NOT_ENUM));
                // Branch 2: isEnum() returns true, so NOT_ENUM returns false
                assertFalse(ClassInfo.of(ClassArrayFormat.class).is(NOT_ENUM));
-
+               
                // LOCAL and NOT_LOCAL (line 1783)
                // Local class
                class LocalTestClass {}
@@ -2302,7 +2302,7 @@ public class ClassInfo_Test extends TestBase {
                assertFalse(localInfo.is(NOT_LOCAL));
                assertTrue(aClass.is(NOT_LOCAL));
                assertFalse(aClass.is(LOCAL));
-
+               
                // NON_STATIC_MEMBER (line 1787)
                // H_PublicMember is a non-static member class
                var nonStaticMember = ClassInfo.of(H_PublicMember.class);
@@ -2310,15 +2310,15 @@ public class ClassInfo_Test extends TestBase {
                assertFalse(nonStaticMember.is(NOT_NON_STATIC_MEMBER));
                assertTrue(aClass.is(NOT_NON_STATIC_MEMBER));
                assertFalse(aClass.is(NON_STATIC_MEMBER));
-
+               
                // PRIMITIVE (line 1789)
                assertTrue(ClassInfo.of(int.class).is(PRIMITIVE));
                assertFalse(aClass.is(PRIMITIVE));
-
+               
                // NOT_PRIMITIVE (line 1790)
                assertTrue(aClass.is(NOT_PRIMITIVE));
                assertFalse(ClassInfo.of(int.class).is(NOT_PRIMITIVE));
-
+               
                // RECORD (line 1791) - test if records are available
                try {
                        Class.forName("java.lang.Record");
@@ -2328,7 +2328,7 @@ public class ClassInfo_Test extends TestBase {
                } catch (ClassNotFoundException e) {
                        // Records not available, skip
                }
-
+               
                // NOT_RECORD (line 1792) - test both branches
                // Branch 1: isRecord() returns false, so NOT_RECORD returns 
true
                assertTrue(aClass.is(NOT_RECORD));
@@ -2342,7 +2342,7 @@ public class ClassInfo_Test extends TestBase {
                } catch (ClassNotFoundException e) {
                        // Records not available, skip
                }
-
+               
                // SEALED (line 1793) - test if sealed classes are available
                try {
                        Class.forName("java.lang.constant.Constable");
@@ -2355,7 +2355,7 @@ public class ClassInfo_Test extends TestBase {
                } catch (ClassNotFoundException e) {
                        // Sealed classes not available, skip
                }
-
+               
                // NOT_SEALED (line 1794) - test both branches
                // Branch 1: isSealed() returns false, so NOT_SEALED returns 
true
                assertTrue(aClass.is(NOT_SEALED));
@@ -2368,7 +2368,7 @@ public class ClassInfo_Test extends TestBase {
                } catch (ClassNotFoundException e) {
                        // Sealed classes not available, skip
                }
-
+               
                // SYNTHETIC (line 1795) - synthetic classes are 
compiler-generated
                // Most regular classes are not synthetic
                assertFalse(aClass.is(SYNTHETIC));
@@ -2379,7 +2379,7 @@ public class ClassInfo_Test extends TestBase {
                        // Just verify the method doesn't throw
                        anonymousInfo.is(SYNTHETIC);
                }
-
+               
                // NOT_SYNTHETIC (line 1796)
                assertTrue(aClass.is(NOT_SYNTHETIC));
        }
@@ -2447,7 +2447,7 @@ public class ClassInfo_Test extends TestBase {
                assertTrue(ClassInfo.of(A.class).isAnnotation());
                assertTrue(ClassInfo.of(B.class).isAnnotation());
                assertFalse(aClass.isAnnotation());
-
+               
                // Test with null inner (line 1811)
                var ci = ClassInfo.of((Class<?>)null, pType);
                assertFalse(ci.isAnnotation());
@@ -2505,16 +2505,16 @@ public class ClassInfo_Test extends TestBase {
                // Test with array
                assertTrue(ClassInfo.of(String[].class).isCollectionOrArray());
                assertTrue(ClassInfo.of(int[].class).isCollectionOrArray());
-
+               
                // Test with Collection
                
assertTrue(ClassInfo.of(java.util.List.class).isCollectionOrArray());
                
assertTrue(ClassInfo.of(java.util.Set.class).isCollectionOrArray());
                
assertTrue(ClassInfo.of(java.util.Collection.class).isCollectionOrArray());
-
+               
                // Test with non-collection, non-array
                assertFalse(aClass.isCollectionOrArray());
                assertFalse(ClassInfo.of(String.class).isCollectionOrArray());
-
+               
                // Test with null inner (line 1905)
                var ci = ClassInfo.of((Class<?>)null, pType);
                assertFalse(ci.isCollectionOrArray());
@@ -2611,7 +2611,7 @@ public class ClassInfo_Test extends TestBase {
        void a081_isEnum() {
                assertTrue(ClassInfo.of(ClassArrayFormat.class).isEnum());
                assertFalse(aClass.isEnum());
-
+               
                // Test with null inner (line 1919)
                var ci = ClassInfo.of((Class<?>)null, pType);
                assertFalse(ci.isEnum());
@@ -2890,10 +2890,10 @@ public class ClassInfo_Test extends TestBase {
                assertFalse(kc.isParentOf(ka));
                assertFalse(kc.isParentOf(kb));
                assertTrue(kc.isParentOf(kc));
-
+               
                // Test with null child (line 2029)
                assertFalse(ka.isParentOf((ClassInfo)null));
-
+               
                // Test with null inner
                var nullInnerCi = ClassInfo.of((Class<?>)null, pType);
                assertFalse(nullInnerCi.isParentOf(ka));
@@ -2929,7 +2929,7 @@ public class ClassInfo_Test extends TestBase {
                
assertFalse(ClassInfo.of(String.class).isParentOfLenient((ClassInfo)null));
                var nullInnerCi = ClassInfo.of((Class<?>)null, pType);
                
assertFalse(nullInnerCi.isParentOfLenient(ClassInfo.of(String.class)));
-
+               
                // Test all branches of line 2087: if (this.isPrimitive() || 
child.isPrimitive())
                // Branch 1: this.isPrimitive() == true, child.isPrimitive() == 
false (already covered above)
                // Branch 2: this.isPrimitive() == false, child.isPrimitive() 
== true (already covered above)
@@ -3020,7 +3020,7 @@ public class ClassInfo_Test extends TestBase {
                        // Records not available, skip test
                        assertFalse(cc3.isRecord());
                }
-
+               
                // Test with null inner (line 2120)
                var ci = ClassInfo.of((Class<?>)null, pType);
                assertFalse(ci.isRecord());
@@ -3093,7 +3093,7 @@ public class ClassInfo_Test extends TestBase {
                var anonymousInfo = ClassInfo.of(anonymous);
                // Anonymous classes are typically synthetic
                assertTrue(anonymousInfo.isSynthetic() || 
!anonymousInfo.isSynthetic());
-
+               
                // Test with null inner (line 2169)
                var ci = ClassInfo.of((Class<?>)null, pType);
                assertFalse(ci.isSynthetic());
@@ -3107,7 +3107,7 @@ public class ClassInfo_Test extends TestBase {
                // Test with null inner (line 2149)
                var ci = ClassInfo.of((Class<?>)null, pType);
                assertFalse(ci.isSealed());
-
+               
                // Test with regular classes (most are not sealed)
                assertFalse(aClass.isSealed());
        }
@@ -3199,13 +3199,13 @@ public class ClassInfo_Test extends TestBase {
                assertNotNull(info);
                assertNull(info.inner());
                assertNotNull(info.innerType());
-
+               
                // Test line 226: isParameterizedType initialization
                // When innerType is null, isParameterizedType should be false
                // String.class is not a ParameterizedType, so 
isParameterizedType should be false
                // We can't directly access isParameterizedType, but we can 
infer it from behavior
                ClassInfo.of(String.class); // Exercise the code path
-
+               
                // When innerType is a ParameterizedType, isParameterizedType 
should be true
                // pTypeInfo has a ParameterizedType, so isParameterizedType 
should be true
                // We can verify this indirectly by checking that it behaves as 
a parameterized type
@@ -3222,7 +3222,7 @@ public class ClassInfo_Test extends TestBase {
                var info = ClassInfo.ofProxy(obj);
                assertNotNull(info);
                assertEquals(A1.class, info.inner());
-
+               
                // Test line 175: when getProxyFor returns null, should call 
ClassInfo.of(object)
                // Most objects are not proxies, so getProxyFor should return 
null
                // This tests the branch: inner == null ? ClassInfo.of(object) 
: ClassInfo.of(inner)
@@ -3263,7 +3263,7 @@ public class ClassInfo_Test extends TestBase {
                mi2 = ClassInfo.of(A6.class).getPublicMethod(x -> 
x.hasName("m2")).get();
                check("A1", 
mi2.getParameter(0).getParameterType().unwrap(Value.class));
                check("A1", mi2.getReturnType().unwrap(Value.class));
-
+               
                // Test unwrap with ParameterizedType (line 2382)
                // Create a ParameterizedType directly
                var pTypeOptional = new java.lang.reflect.ParameterizedType() {
@@ -3282,7 +3282,7 @@ public class ClassInfo_Test extends TestBase {
                };
                var ciOptional = ClassInfo.of((Class<?>)null, pTypeOptional);
                check("A1", ciOptional.unwrap(Optional.class));
-
+               
                // Test unwrap with ParameterizedType that has no type 
arguments (line 2383)
                var pTypeEmpty = new java.lang.reflect.ParameterizedType() {
                        @Override
@@ -3301,22 +3301,22 @@ public class ClassInfo_Test extends TestBase {
                var ciEmpty = ClassInfo.of((Class<?>)null, pTypeEmpty);
                // Should return itself since there are no type arguments
                assertSame(ciEmpty, ciEmpty.unwrap(Optional.class));
-
+               
                // Test unwrap with Class that extends wrapper (line 2387, 2388)
                // A2 extends Value<A1>, so unwrap should work
                // This covers: innerType instanceof Class<?> is true, 
innerType3 != parameterizedType is true, isAssignableFrom is true
                check("A1", of(A2.class).unwrap(Value.class));
-
+               
                // Test unwrap with Class that doesn't extend wrapper (line 
2388 - false branch)
                // A1 doesn't extend Value, so unwrap should return itself
                // This covers: innerType instanceof Class<?> is true, 
innerType3 != parameterizedType is true, isAssignableFrom is false
                assertSame(of(A1.class), of(A1.class).unwrap(Value.class));
-
+               
                // Test unwrap when innerType3 == parameterizedType (line 2388 
- false branch of !=)
                // When unwrapping Value.class from Value.class itself, 
innerType3 == parameterizedType, so should return itself
                // This covers: innerType instanceof Class<?> is true, 
innerType3 != parameterizedType is false (short-circuit)
                assertSame(of(Value.class), 
of(Value.class).unwrap(Value.class));
-
+               
                // Test unwrap when innerType is not a Class<?> (line 2387 - 
false branch)
                // When innerType is a ParameterizedType, the else if branch is 
not entered
                // This is already covered by the ParameterizedType tests 
above, but let's verify
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/utils/GranularZonedDateTime_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/utils/GranularZonedDateTime_Test.java
index 11f5e08c8c..c77d9a0f47 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/utils/GranularZonedDateTime_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/utils/GranularZonedDateTime_Test.java
@@ -268,25 +268,25 @@ class GranularZonedDateTime_Test extends TestBase {
        void f03_rollWithField_allSupportedFields() {
                var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 123000000, 
ZoneId.of("UTC"));
                var gdt = new GranularZonedDateTime(zdt, 
ChronoField.MILLI_OF_SECOND);
-               
+
                var rolled1 = gdt.roll(ChronoField.YEAR, 1);
                assertEquals(zdt.plusYears(1), rolled1.zdt);
-               
+
                var rolled2 = gdt.roll(ChronoField.MONTH_OF_YEAR, 1);
                assertEquals(zdt.plusMonths(1), rolled2.zdt);
-               
+
                var rolled3 = gdt.roll(ChronoField.DAY_OF_MONTH, 1);
                assertEquals(zdt.plusDays(1), rolled3.zdt);
-               
+
                var rolled4 = gdt.roll(ChronoField.HOUR_OF_DAY, 1);
                assertEquals(zdt.plusHours(1), rolled4.zdt);
-               
+
                var rolled5 = gdt.roll(ChronoField.MINUTE_OF_HOUR, 1);
                assertEquals(zdt.plusMinutes(1), rolled5.zdt);
-               
+
                var rolled6 = gdt.roll(ChronoField.SECOND_OF_MINUTE, 1);
                assertEquals(zdt.plusSeconds(1), rolled6.zdt);
-               
+
                var rolled7 = gdt.roll(ChronoField.MILLI_OF_SECOND, 1);
                assertEquals(zdt.plus(1, ChronoUnit.MILLIS), rolled7.zdt);
        }
@@ -425,14 +425,12 @@ class GranularZonedDateTime_Test extends TestBase {
 
        @Test
        void g11_parse_null() {
-               assertThrowsWithMessage(RuntimeException.class, "Invalid date", 
() -> {
-                       GranularZonedDateTime.parse(null);
-               });
+               assertThrows(IllegalArgumentException.class, () -> 
GranularZonedDateTime.parse(null));
        }
 
        @Test
        void g12_parse_emptyString() {
-               assertThrowsWithMessage(RuntimeException.class, "Invalid date", 
() -> {
+               assertThrowsWithMessage(RuntimeException.class, "Invalid 
ISO8601 timestamp", () -> {
                        GranularZonedDateTime.parse("");
                });
        }
@@ -476,5 +474,1083 @@ class GranularZonedDateTime_Test extends TestBase {
                assertEquals(15, gdt1.zdt.getDayOfMonth());
                assertEquals(16, rolled.zdt.getDayOfMonth());
        }
+
+       
//====================================================================================================
+       // parse2(String) tests
+       
//====================================================================================================
+
+       @Test
+       void i01_parse2_null() {
+               // parse2(String) overload throws IllegalArgumentException when 
seg is null
+               assertThrows(IllegalArgumentException.class, () -> {
+                       GranularZonedDateTime.parse2((String)null);
+               });
+               // parse2(String, ZoneId) also throws when seg is null
+               assertThrows(IllegalArgumentException.class, () -> {
+                       GranularZonedDateTime.parse2((String)null, 
(ZoneId)null);
+               });
+       }
+
+       @Test
+       void i02_parse2_yearOnly() {
+               var gdt = GranularZonedDateTime.parse2("2011", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(1, gdt.zdt.getDayOfMonth());
+               assertEquals(0, gdt.zdt.getHour());
+               assertEquals(ChronoField.YEAR, gdt.precision);
+       }
+
+       @Test
+       void i03_parse2_yearMonth() {
+               var gdt = GranularZonedDateTime.parse2("2011-01", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(1, gdt.zdt.getDayOfMonth());
+               assertEquals(ChronoField.MONTH_OF_YEAR, gdt.precision);
+       }
+
+       @Test
+       void i04_parse2_date() {
+               var gdt = GranularZonedDateTime.parse2("2011-01-15", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(15, gdt.zdt.getDayOfMonth());
+               assertEquals(ChronoField.DAY_OF_MONTH, gdt.precision);
+       }
+
+       @Test
+       void i05_parse2_dateTime_hour() {
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(15, gdt.zdt.getDayOfMonth());
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(0, gdt.zdt.getMinute());
+               assertEquals(ChronoField.HOUR_OF_DAY, gdt.precision);
+       }
+
+       @Test
+       void i06_parse2_dateTime_minute() {
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30", 
null);
+               assertNotNull(gdt);
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(0, gdt.zdt.getSecond());
+               assertEquals(ChronoField.MINUTE_OF_HOUR, gdt.precision);
+       }
+
+       @Test
+       void i07_parse2_dateTime_second() {
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30:45", 
null);
+               assertNotNull(gdt);
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(45, gdt.zdt.getSecond());
+               assertEquals(ChronoField.SECOND_OF_MINUTE, gdt.precision);
+       }
+
+       @Test
+       void i08_parse2_dateTime_millisecond_dot() {
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.123", null);
+               assertNotNull(gdt);
+               assertEquals(45, gdt.zdt.getSecond());
+               assertEquals(123000000, gdt.zdt.getNano());
+               assertEquals(ChronoField.MILLI_OF_SECOND, gdt.precision);
+       }
+
+       @Test
+       void i09_parse2_dateTime_millisecond_comma() {
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45,123", null);
+               assertNotNull(gdt);
+               assertEquals(45, gdt.zdt.getSecond());
+               assertEquals(123000000, gdt.zdt.getNano());
+               assertEquals(ChronoField.MILLI_OF_SECOND, gdt.precision);
+       }
+
+       @Test
+       void i10_parse2_timeOnly_hour() {
+               var now = ZonedDateTime.now();
+               var gdt = GranularZonedDateTime.parse2("T12", null);
+               assertNotNull(gdt);
+               assertEquals(now.getYear(), gdt.zdt.getYear());
+               assertEquals(now.getMonthValue(), gdt.zdt.getMonthValue());
+               assertEquals(now.getDayOfMonth(), gdt.zdt.getDayOfMonth());
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(0, gdt.zdt.getMinute());
+               assertEquals(ChronoField.HOUR_OF_DAY, gdt.precision);
+       }
+
+       @Test
+       void i11_parse2_timeOnly_minute() {
+               var now = ZonedDateTime.now();
+               var gdt = GranularZonedDateTime.parse2("T12:30", null);
+               assertNotNull(gdt);
+               assertEquals(now.getYear(), gdt.zdt.getYear());
+               assertEquals(now.getMonthValue(), gdt.zdt.getMonthValue());
+               assertEquals(now.getDayOfMonth(), gdt.zdt.getDayOfMonth());
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(ChronoField.MINUTE_OF_HOUR, gdt.precision);
+       }
+
+       @Test
+       void i12_parse2_timeOnly_second() {
+               var now = ZonedDateTime.now();
+               var gdt = GranularZonedDateTime.parse2("T12:30:45", null);
+               assertNotNull(gdt);
+               assertEquals(now.getYear(), gdt.zdt.getYear());
+               assertEquals(now.getMonthValue(), gdt.zdt.getMonthValue());
+               assertEquals(now.getDayOfMonth(), gdt.zdt.getDayOfMonth());
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(45, gdt.zdt.getSecond());
+               assertEquals(ChronoField.SECOND_OF_MINUTE, gdt.precision);
+       }
+
+       @Test
+       void i13_parse2_timeOnly_millisecond() {
+               var now = ZonedDateTime.now();
+               var gdt = GranularZonedDateTime.parse2("T12:30:45.123", null);
+               assertNotNull(gdt);
+               assertEquals(now.getYear(), gdt.zdt.getYear());
+               assertEquals(now.getMonthValue(), gdt.zdt.getMonthValue());
+               assertEquals(now.getDayOfMonth(), gdt.zdt.getDayOfMonth());
+               assertEquals(123000000, gdt.zdt.getNano());
+               assertEquals(ChronoField.MILLI_OF_SECOND, gdt.precision);
+       }
+
+       @Test
+       void i14_parse2_withZ() {
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30:45Z", 
null);
+               assertNotNull(gdt);
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+               assertEquals(12, gdt.zdt.getHour());
+       }
+
+       @Test
+       void i15_parse2_withOffset_plusHH() {
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45+05", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i16_parse2_withOffset_minusHH() {
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45-05", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i17_parse2_withOffset_plusHHMM() {
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45+0530", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHoursMinutes(5, 30), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i18_parse2_withOffset_minusHHMM() {
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45-0530", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHoursMinutes(-5, -30), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i19_parse2_withOffset_plusHH_MM() {
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45+05:30", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHoursMinutes(5, 30), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i20_parse2_withOffset_minusHH_MM() {
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45-05:30", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHoursMinutes(-5, -30), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i21_parse2_timezoneAfterYear() {
+               var gdt = GranularZonedDateTime.parse2("2011Z", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i22_parse2_timezoneAfterMonth() {
+               var gdt = GranularZonedDateTime.parse2("2011-01Z", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i23_parse2_timezoneAfterDay() {
+               var gdt = GranularZonedDateTime.parse2("2011-01-15Z", null);
+               assertNotNull(gdt);
+               assertEquals(15, gdt.zdt.getDayOfMonth());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i24_parse2_timezoneAfterHour() {
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12Z", null);
+               assertNotNull(gdt);
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i25_parse2_timezoneAfterMinute() {
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30Z", 
null);
+               assertNotNull(gdt);
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i26_parse2_timezoneAfterT() {
+               var gdt = GranularZonedDateTime.parse2("2011-01T+05:30", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(ZoneOffset.ofHoursMinutes(5, 30), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i27_parse2_nanoseconds_1digit() {
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30:45.1", 
null);
+               assertNotNull(gdt);
+               assertEquals(100000000, gdt.zdt.getNano());
+               // 1 digit is treated as milliseconds (hundreds of milliseconds)
+               assertEquals(ChronoField.MILLI_OF_SECOND, gdt.precision);
+       }
+
+       @Test
+       void i28_parse2_nanoseconds_2digits() {
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.12", null);
+               assertNotNull(gdt);
+               assertEquals(120000000, gdt.zdt.getNano());
+       }
+
+       @Test
+       void i29_parse2_nanoseconds_3digits() {
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.123", null);
+               assertNotNull(gdt);
+               assertEquals(123000000, gdt.zdt.getNano());
+       }
+
+       @Test
+       void i30_parse2_nanoseconds_4digits() {
+               // Lines 1018: len == 4
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.1234", null);
+               assertNotNull(gdt);
+               assertEquals(123400000, gdt.zdt.getNano());
+               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
+       }
+
+       @Test
+       void i30a_parse2_nanoseconds_5digits() {
+               // Lines 1019: len == 5
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.12345", null);
+               assertNotNull(gdt);
+               assertEquals(123450000, gdt.zdt.getNano());
+               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
+       }
+
+       @Test
+       void i30b_parse2_nanoseconds_6digits() {
+               // Lines 1020: len == 6
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.123456", null);
+               assertNotNull(gdt);
+               assertEquals(123456000, gdt.zdt.getNano());
+               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
+       }
+
+       @Test
+       void i30c_parse2_nanoseconds_7digits() {
+               // Lines 1021: len == 7
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.1234567", null);
+               assertNotNull(gdt);
+               assertEquals(123456700, gdt.zdt.getNano());
+               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
+       }
+
+       @Test
+       void i30d_parse2_nanoseconds_8digits() {
+               // Lines 1022: len == 8
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.12345678", null);
+               assertNotNull(gdt);
+               assertEquals(123456780, gdt.zdt.getNano());
+               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
+       }
+
+       @Test
+       void i30e_parse2_nanoseconds_9digits() {
+               // Line 1023: len == 9 (default return)
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.123456789", null);
+               assertNotNull(gdt);
+               assertEquals(123456789, gdt.zdt.getNano());
+               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
+       }
+
+       @Test
+       void i31_parse2_badTimestamps() {
+               // Invalid formats
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("invalid", null));
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("", null));
+
+               // Invalid year length
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("123", null));
+
+               // Invalid month - below minimum (0)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-00", null));
+
+               // Invalid month - above maximum (13)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-13", null));
+
+               // Invalid month - way above maximum
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-99", null));
+
+               // Invalid day - below minimum (0)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-00", null));
+
+               // Invalid day - above maximum (32)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-32", null));
+
+               // Invalid day - way above maximum
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-99", null));
+
+               // Invalid hour - below minimum (-1, but this would be caught 
as invalid format)
+               // Invalid hour - above maximum (24)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T24", null));
+
+               // Invalid hour - way above maximum
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T99", null));
+
+               // Invalid minute - above maximum (60)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:60", null));
+
+               // Invalid minute - way above maximum
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:99", null));
+
+               // Invalid second - above maximum (60)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:60", null));
+
+               // Invalid second - way above maximum
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:99", null));
+
+               // Invalid character after year (line 642)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011X", null));
+
+               // Invalid character after '-' in S3 (line 651)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-X", null));
+
+               // Invalid character after month (line 676)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01X", null));
+
+               // Invalid character after '-' in S5 (line 685)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-X", null));
+
+               // Invalid character after day (line 707)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15X", null));
+
+               // Invalid character after 'T' in S7 (line 725)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("TX", null));
+
+               // Invalid character after hour (line 747)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12X", null));
+
+               // Invalid character after ':' in S9 (line 756)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:X", null));
+
+               // Invalid character after minute (line 778)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30X", null));
+
+               // Invalid character after ':' in S11 (line 787)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:X", null));
+
+               // Invalid character in S12 after seconds (line 810)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45X", null));
+
+               // Invalid character in S13 after '.' or ',' (line 827)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45.X", null));
+
+               // Invalid character in S14 while reading milliseconds (line 
846)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45.12X", null));
+
+               // Invalid character in S16 after '+' (line 857)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45+X", null));
+
+               // Invalid character in S17 after '-' (line 865)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45-X", null));
+
+               // Invalid character in S18 while reading offset hours (line 
875)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45+05X", null));
+
+               // Invalid character in S19 after ':' in offset (line 884)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45+05:X", null));
+
+               // Invalid character in S20 while reading offset minutes (line 
891)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45+05:30X", null));
+
+               // Invalid offset format in S18 finalization - not 2 or 4 
digits (line 941)
+               // Ending in S18 with 1 digit (should be 2 or 4)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45+1", null));
+               // Ending in S18 with 3 digits (should be 2 or 4)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45+123", null));
+               // Ending in S18 with 5 digits (should be 2 or 4)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45+12345", null));
+
+               // Invalid nanosecond length in parseNanos (line 1010)
+               // This is defensive code - parseNanos checks length is 1-9 
digits
+               // The state machine should prevent this, but we test the 
defensive check
+               // by ending in S14 with 0 digits (which shouldn't happen, but 
tests the check)
+               // Actually, we can't easily trigger 0 digits since we 
transition to S14 only after seeing a digit.
+               // For >9 digits, if we have 10+ digits, the 10th non-digit 
would trigger line 846, not 1010.
+               // However, if we end the string with exactly 10 digits, we'd 
call parseNanos with len=10, triggering 1010.
+               // But wait, the state machine allows digits in S14, so we'd 
need to end the string with 10+ digits.
+               // Let's test with a string that ends with 10 digits of 
fractional seconds (no timezone)
+               // This would end in S14 with 10 digits, calling parseNanos 
with len=10, which is >9, triggering line 1010.
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.parse2("2011-01-15T12:30:45.1234567890", null));
+       }
+
+       @Test
+       void i38_parse2_timeOnly_withZ() {
+               var now = ZonedDateTime.now();
+               var gdt = GranularZonedDateTime.parse2("T12:30:45Z", null);
+               assertNotNull(gdt);
+               assertEquals(now.getYear(), gdt.zdt.getYear());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i39_parse2_timeOnly_withOffset() {
+               var now = ZonedDateTime.now();
+               var gdt = GranularZonedDateTime.parse2("T12:30:45+05:30", null);
+               assertNotNull(gdt);
+               assertEquals(now.getYear(), gdt.zdt.getYear());
+               assertEquals(ZoneOffset.ofHoursMinutes(5, 30), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i40_parse2_noTimezone_usesSystemDefault() {
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30:45", 
null);
+               assertNotNull(gdt);
+               assertEquals(ZoneId.systemDefault(), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i41_parse2_yearFollowedByT() {
+               // Lines 627-628: Year followed by 'T'
+               var gdt = GranularZonedDateTime.parse2("2011T12", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(12, gdt.zdt.getHour());
+       }
+
+       @Test
+       void i42_parse2_yearFollowedByZ() {
+               // Lines 629-632: Year followed by 'Z'
+               var gdt = GranularZonedDateTime.parse2("2011Z", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i43_parse2_yearFollowedByPlus() {
+               // Lines 633-636: Year followed by '+'
+               var gdt = GranularZonedDateTime.parse2("2011+05", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i44_parse2_yearFollowedByMinus() {
+               // Lines 637-640: Year followed by '-' (timezone)
+               // Note: The code has two '-' checks in S2, but the first one 
(line 621) goes to S3 (month),
+               // so the second '-' check (line 637) is unreachable in normal 
parsing.
+               // However, we can test the negative timezone path by using a 
format that works:
+               // After parsing a complete component, we can have timezone. 
Let's test with hour followed by negative timezone.
+               // Actually, let's test the negative timezone path that IS 
reachable - after hour, minute, or second.
+               // This test covers the concept even if the specific line isn't 
reachable.
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12-05", 
null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i45_parse2_monthFollowedByZ() {
+               // Lines 663-666: Month followed by 'Z'
+               var gdt = GranularZonedDateTime.parse2("2011-01Z", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i46_parse2_monthFollowedByPlus() {
+               // Lines 667-670: Month followed by '+'
+               var gdt = GranularZonedDateTime.parse2("2011-01+05", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i47_parse2_monthFollowedByMinus() {
+               // Lines 671-674: Month followed by '-' (timezone)
+               // Note: Similar to i44, the second '-' check in S4 may be 
unreachable because the first '-' goes to S5 (day).
+               // However, we can test the negative timezone concept with a 
reachable path.
+               // Let's test with minute followed by negative timezone to 
cover the negative timezone logic.
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30-05", 
null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i48_parse2_dayFollowedByZ() {
+               // Lines 692-697: Day followed by 'Z'
+               var gdt = GranularZonedDateTime.parse2("2011-01-15Z", null);
+               assertNotNull(gdt);
+               assertEquals(15, gdt.zdt.getDayOfMonth());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i49_parse2_dayFollowedByPlus() {
+               // Lines 698-701: Day followed by '+'
+               var gdt = GranularZonedDateTime.parse2("2011-01-15+05", null);
+               assertNotNull(gdt);
+               assertEquals(15, gdt.zdt.getDayOfMonth());
+               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i50_parse2_dayFollowedByMinus() {
+               // Lines 702-705: Day followed by '-'
+               var gdt = GranularZonedDateTime.parse2("2011-01-15-05", null);
+               assertNotNull(gdt);
+               assertEquals(15, gdt.zdt.getDayOfMonth());
+               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i51_parse2_TFollowedByZ() {
+               // Lines 715-717: 'T' followed by 'Z'
+               var gdt = GranularZonedDateTime.parse2("TZ", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i52_parse2_TFollowedByPlus() {
+               // Lines 718-720: 'T' followed by '+'
+               var gdt = GranularZonedDateTime.parse2("T+05", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i53_parse2_TFollowedByMinus() {
+               // Lines 721-723: 'T' followed by '-'
+               var gdt = GranularZonedDateTime.parse2("T-05", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i54_parse2_hourFollowedByZ() {
+               // Lines 732-737: Hour followed by 'Z'
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12Z", null);
+               assertNotNull(gdt);
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i55_parse2_hourFollowedByPlus() {
+               // Lines 738-741: Hour followed by '+'
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12+05", 
null);
+               assertNotNull(gdt);
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i56_parse2_hourFollowedByMinus() {
+               // Lines 742-745: Hour followed by '-'
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12-05", 
null);
+               assertNotNull(gdt);
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i57_parse2_minuteFollowedByZ() {
+               // Lines 765-768: Minute followed by 'Z'
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30Z", 
null);
+               assertNotNull(gdt);
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i58_parse2_minuteFollowedByPlus() {
+               // Lines 769-772: Minute followed by '+'
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30+05", 
null);
+               assertNotNull(gdt);
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i59_parse2_minuteFollowedByMinus() {
+               // Lines 773-776: Minute followed by '-'
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30-05", 
null);
+               assertNotNull(gdt);
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
+       }
+
+       
//====================================================================================================
+       // parse2(String) - Timezone after fractional seconds tests
+       
//====================================================================================================
+
+       @Test
+       void i73_parse2_S13_fractionalSeparatorFollowedByZ() {
+               // Lines 817-819: S13 - '.' or ',' followed by 'Z'
+               var gdt1 = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.Z", null);
+               assertNotNull(gdt1);
+               assertEquals(ZoneId.of("Z"), gdt1.zdt.getZone());
+               assertEquals(0, gdt1.zdt.getNano());
+
+               var gdt2 = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45,Z", null);
+               assertNotNull(gdt2);
+               assertEquals(ZoneId.of("Z"), gdt2.zdt.getZone());
+               assertEquals(0, gdt2.zdt.getNano());
+       }
+
+       @Test
+       void i74_parse2_S13_fractionalSeparatorFollowedByPlus() {
+               // Lines 820-822: S13 - '.' or ',' followed by '+'
+               var gdt1 = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.+05:00", null);
+               assertNotNull(gdt1);
+               assertEquals(ZoneOffset.ofHours(5), gdt1.zdt.getOffset());
+               assertEquals(0, gdt1.zdt.getNano());
+
+               var gdt2 = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45,+05:00", null);
+               assertNotNull(gdt2);
+               assertEquals(ZoneOffset.ofHours(5), gdt2.zdt.getOffset());
+               assertEquals(0, gdt2.zdt.getNano());
+       }
+
+       @Test
+       void i75_parse2_S13_fractionalSeparatorFollowedByMinus() {
+               // Lines 823-825: S13 - '.' or ',' followed by '-'
+               var gdt1 = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.-05:00", null);
+               assertNotNull(gdt1);
+               assertEquals(ZoneOffset.ofHours(-5), gdt1.zdt.getOffset());
+               assertEquals(0, gdt1.zdt.getNano());
+
+               var gdt2 = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45,-05:00", null);
+               assertNotNull(gdt2);
+               assertEquals(ZoneOffset.ofHours(-5), gdt2.zdt.getOffset());
+               assertEquals(0, gdt2.zdt.getNano());
+       }
+
+       @Test
+       void i76_parse2_S14_fractionalSecondsFollowedByZ() {
+               // Lines 833-836: S14 - fractional seconds followed by 'Z'
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.123Z", null);
+               assertNotNull(gdt);
+               assertEquals(123000000, gdt.zdt.getNano());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i77_parse2_S14_fractionalSecondsFollowedByPlus() {
+               // Lines 837-840: S14 - fractional seconds followed by '+'
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.123+05:00", null);
+               assertNotNull(gdt);
+               assertEquals(123000000, gdt.zdt.getNano());
+               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i78_parse2_S14_fractionalSecondsFollowedByMinus() {
+               // Lines 841-844: S14 - fractional seconds followed by '-'
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45.123-05:00", null);
+               assertNotNull(gdt);
+               assertEquals(123000000, gdt.zdt.getNano());
+               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i79_parse2_S14_fractionalSecondsWithCommaFollowedByZ() {
+               // Lines 833-836: S14 - fractional seconds (comma separator) 
followed by 'Z'
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45,123Z", null);
+               assertNotNull(gdt);
+               assertEquals(123000000, gdt.zdt.getNano());
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+       }
+
+       @Test
+       void i80_parse2_S14_fractionalSecondsWithCommaFollowedByPlus() {
+               // Lines 837-840: S14 - fractional seconds (comma separator) 
followed by '+'
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45,123+05:00", null);
+               assertNotNull(gdt);
+               assertEquals(123000000, gdt.zdt.getNano());
+               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i81_parse2_S14_fractionalSecondsWithCommaFollowedByMinus() {
+               // Lines 841-844: S14 - fractional seconds (comma separator) 
followed by '-'
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45,123-05:00", null);
+               assertNotNull(gdt);
+               assertEquals(123000000, gdt.zdt.getNano());
+               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i82_parse2_S15_invalidCharacterAfterZ() {
+               // Line 846: S15 - invalid character after 'Z' (should throw 
error)
+               // After finding 'Z', any additional characters should trigger 
this error
+               // Test all possible transitions to S15:
+
+               // S2 -> S15 (year followed by Z, then invalid char)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011Z5", null);
+               });
+
+               // S4 -> S15 (month followed by Z, then invalid char)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01ZX", null);
+               });
+
+               // S6 -> S15 (day followed by Z, then invalid char)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15ZX", null);
+               });
+
+               // S7 -> S15 (T followed by Z, then invalid char)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("TZX", null);
+               });
+
+               // S8 -> S15 (hour followed by Z, then invalid char)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15T12ZX", null);
+               });
+
+               // S10 -> S15 (minute followed by Z, then invalid char)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15T12:30ZX", 
null);
+               });
+
+               // S12 -> S15 (second followed by Z, then invalid char)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15T12:30:45ZX", 
null);
+               });
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15T12:30:45Z+", 
null);
+               });
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15T12:30:45Z-", 
null);
+               });
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15T12:30:45Z5", 
null);
+               });
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15T12:30:45Z ", 
null);
+               });
+
+               // S13 -> S15 (fractional separator followed by Z, then invalid 
char)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15T12:30:45.ZX", 
null);
+               });
+
+               // S14 -> S15 (fractional digits followed by Z, then invalid 
char)
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       
GranularZonedDateTime.parse2("2011-01-15T12:30:45.123ZX", null);
+               });
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("T12:30:45ZX", null);
+               });
+       }
+
+       @Test
+       void i83_parse2_invalidDateForMonth() {
+               // Invalid dates for specific months - LocalDateTime.of() 
throws DateTimeException
+               // November only has 30 days
+               assertThrows(java.time.DateTimeException.class, () -> {
+                       GranularZonedDateTime.parse2("2011-11-31", null);
+               });
+               // February in non-leap year only has 28 days
+               assertThrows(java.time.DateTimeException.class, () -> {
+                       GranularZonedDateTime.parse2("2011-02-29", null);
+               });
+               // February in non-leap year only has 28 days (day 30)
+               assertThrows(java.time.DateTimeException.class, () -> {
+                       GranularZonedDateTime.parse2("2011-02-30", null);
+               });
+               // April only has 30 days
+               assertThrows(java.time.DateTimeException.class, () -> {
+                       GranularZonedDateTime.parse2("2011-04-31", null);
+               });
+               // June only has 30 days
+               assertThrows(java.time.DateTimeException.class, () -> {
+                       GranularZonedDateTime.parse2("2011-06-31", null);
+               });
+               // September only has 30 days
+               assertThrows(java.time.DateTimeException.class, () -> {
+                       GranularZonedDateTime.parse2("2011-09-31", null);
+               });
+               // Valid: February 29 in leap year
+               var gdt = GranularZonedDateTime.parse2("2024-02-29", null);
+               assertNotNull(gdt);
+               assertEquals(2024, gdt.zdt.getYear());
+               assertEquals(2, gdt.zdt.getMonthValue());
+               assertEquals(29, gdt.zdt.getDayOfMonth());
+       }
+
+       @Test
+       void i84_parse2_zoneIdAlreadySet() {
+               // Line 941: zoneId is not null, so offset building is skipped 
(false branch)
+               // Test with 'Z' timezone (zoneId already set)
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30:45Z", 
null);
+               assertNotNull(gdt);
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+               // ohour and ominute remain -1, but zoneId is already set, so 
the if condition on line 941 is false
+       }
+
+       @Test
+       void i85_parse2_offsetOnlyHours() {
+               // Line 946: ohour >= 0 but ominute < 0 (only hours, no 
minutes) - else if branch
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45+05", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i86_parse2_offsetHoursAndMinutes() {
+               // Line 942: ohour >= 0 && ominute >= 0 (both hours and minutes)
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45+05:30", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHoursMinutes(5, 30), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i87_parse2_timeOnlyMissingYear() {
+               // Lines 964-966: timeOnly=true, year/month/day are -1, use 
current date
+               var now = ZonedDateTime.now();
+               var gdt = GranularZonedDateTime.parse2("T12:30:45", null);
+               assertNotNull(gdt);
+               assertEquals(now.getYear(), gdt.zdt.getYear());
+               assertEquals(now.getMonthValue(), gdt.zdt.getMonthValue());
+               assertEquals(now.getDayOfMonth(), gdt.zdt.getDayOfMonth());
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(45, gdt.zdt.getSecond());
+       }
+
+       @Test
+       void i88_parse2_dateFormatMissingMonth() {
+               // Line 970: timeOnly=false, month is -1, default to 1
+               var gdt = GranularZonedDateTime.parse2("2011", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue()); // Defaults to 1
+               assertEquals(1, gdt.zdt.getDayOfMonth()); // Defaults to 1
+       }
+
+       @Test
+       void i89_parse2_dateFormatMissingDay() {
+               // Line 971: timeOnly=false, day is -1, default to 1
+               var gdt = GranularZonedDateTime.parse2("2011-01", null);
+               assertNotNull(gdt);
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(1, gdt.zdt.getDayOfMonth()); // Defaults to 1
+       }
+
+       @Test
+       void i90_parse2_withDefaultZoneId() {
+               // Line 958: defaultZoneId != null branch - use provided 
defaultZoneId when no zone in string
+               var defaultZone = ZoneId.of("America/New_York");
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30:45", 
defaultZone);
+               assertNotNull(gdt);
+               assertEquals(defaultZone, gdt.zdt.getZone());
+               assertEquals(2011, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(15, gdt.zdt.getDayOfMonth());
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(45, gdt.zdt.getSecond());
+       }
+
+       @Test
+       void i91_parse2_withDefaultZoneId_timeOnly() {
+               // Line 958: defaultZoneId != null branch - use provided 
defaultZoneId for time-only format
+               var defaultZone = ZoneId.of("Europe/London");
+               var now = ZonedDateTime.now(defaultZone);
+               var gdt = GranularZonedDateTime.parse2("T12:30:45", 
defaultZone);
+               assertNotNull(gdt);
+               assertEquals(defaultZone, gdt.zdt.getZone());
+               assertEquals(now.getYear(), gdt.zdt.getYear());
+               assertEquals(now.getMonthValue(), gdt.zdt.getMonthValue());
+               assertEquals(now.getDayOfMonth(), gdt.zdt.getDayOfMonth());
+               assertEquals(12, gdt.zdt.getHour());
+               assertEquals(30, gdt.zdt.getMinute());
+               assertEquals(45, gdt.zdt.getSecond());
+       }
+
+       @Test
+       void i92_parse2_withDefaultZoneId_ignoredWhenZoneInString() {
+               // Line 958: defaultZoneId is ignored when zone is found in the 
string
+               var defaultZone = ZoneId.of("America/New_York");
+               var gdt = GranularZonedDateTime.parse2("2011-01-15T12:30:45Z", 
defaultZone);
+               assertNotNull(gdt);
+               // Should use 'Z' from the string, not the defaultZoneId
+               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
+               assertNotEquals(defaultZone, gdt.zdt.getZone());
+       }
+
+       
//====================================================================================================
+       // parse2(String) - ISO8601 offset range validation tests (-18:00 ≤ 
offset ≤ +18:00)
+       
//====================================================================================================
+
+       @Test
+       void i60_parse2_offsetBoundary_minus18_00() {
+               // Minimum valid offset: -18:00
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45-18:00", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHoursMinutes(-18, 0), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i61_parse2_offsetBoundary_plus18_00() {
+               // Maximum valid offset: +18:00
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45+18:00", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHoursMinutes(18, 0), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i62_parse2_offsetBoundary_minus18_00_compact() {
+               // Minimum valid offset in compact format: -1800
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45-1800", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHoursMinutes(-18, 0), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i63_parse2_offsetBoundary_plus18_00_compact() {
+               // Maximum valid offset in compact format: +1800
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45+1800", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHoursMinutes(18, 0), 
gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i64_parse2_offsetBoundary_minus18_00_hoursOnly() {
+               // Minimum valid offset hours only: -18
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45-18", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHours(-18), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i65_parse2_offsetBoundary_plus18_00_hoursOnly() {
+               // Maximum valid offset hours only: +18
+               var gdt = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45+18", null);
+               assertNotNull(gdt);
+               assertEquals(ZoneOffset.ofHours(18), gdt.zdt.getOffset());
+       }
+
+       @Test
+       void i66_parse2_offsetInvalid_belowMinimum() {
+               // Invalid: offset below -18:00
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       
GranularZonedDateTime.parse2("2011-01-15T12:30:45-19:00", null);
+               });
+       }
+
+       @Test
+       void i67_parse2_offsetInvalid_aboveMaximum() {
+               // Invalid: offset above +18:00
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       
GranularZonedDateTime.parse2("2011-01-15T12:30:45+19:00", null);
+               });
+       }
+
+       @Test
+       void i68_parse2_offsetInvalid_belowMinimum_compact() {
+               // Invalid: offset below -18:00 in compact format
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       
GranularZonedDateTime.parse2("2011-01-15T12:30:45-1900", null);
+               });
+       }
+
+       @Test
+       void i69_parse2_offsetInvalid_aboveMaximum_compact() {
+               // Invalid: offset above +18:00 in compact format
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       
GranularZonedDateTime.parse2("2011-01-15T12:30:45+1900", null);
+               });
+       }
+
+       @Test
+       void i70_parse2_offsetInvalid_belowMinimum_hoursOnly() {
+               // Invalid: offset below -18 hours only
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15T12:30:45-19", 
null);
+               });
+       }
+
+       @Test
+       void i71_parse2_offsetInvalid_aboveMaximum_hoursOnly() {
+               // Invalid: offset above +18 hours only
+               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
+                       GranularZonedDateTime.parse2("2011-01-15T12:30:45+19", 
null);
+               });
+       }
+
+       @Test
+       void i72_parse2_offsetValid_withinRange() {
+               // Valid offsets within range
+               var gdt1 = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45-12:30", null);
+               assertNotNull(gdt1);
+               assertEquals(ZoneOffset.ofHoursMinutes(-12, -30), 
gdt1.zdt.getOffset());
+
+               var gdt2 = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45+12:30", null);
+               assertNotNull(gdt2);
+               assertEquals(ZoneOffset.ofHoursMinutes(12, 30), 
gdt2.zdt.getOffset());
+
+               var gdt3 = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45-17:59", null);
+               assertNotNull(gdt3);
+               assertEquals(ZoneOffset.ofHoursMinutes(-17, -59), 
gdt3.zdt.getOffset());
+
+               var gdt4 = 
GranularZonedDateTime.parse2("2011-01-15T12:30:45+17:59", null);
+               assertNotNull(gdt4);
+               assertEquals(ZoneOffset.ofHoursMinutes(17, 59), 
gdt4.zdt.getOffset());
+       }
 }
 

Reply via email to