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 1a4d8565f7 Unit tests
1a4d8565f7 is described below

commit 1a4d8565f7d1b3ed26f18d5115fa0dd66d474f70
Author: James Bognar <[email protected]>
AuthorDate: Thu Dec 4 10:46:47 2025 -0800

    Unit tests
---
 .../commons/function/ResettableSupplier.java       |    5 +
 .../juneau/commons/time/GranularZonedDateTime.java |  231 ++-
 .../apache/juneau/commons/time/TimeProvider.java   |   41 +
 .../commons/time/GranularZonedDateTime_Test.java   | 1653 ++++----------------
 .../java/org/apache/juneau/oapi/OpenApi_Test.java  |    5 +-
 .../juneau/utest/utils/FakeTimeProvider.java       |   46 +
 6 files changed, 542 insertions(+), 1439 deletions(-)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/function/ResettableSupplier.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/function/ResettableSupplier.java
index bfaaad94b5..6c655448ec 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/function/ResettableSupplier.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/function/ResettableSupplier.java
@@ -19,6 +19,7 @@ package org.apache.juneau.commons.function;
 import static org.apache.juneau.commons.utils.AssertionUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 
+import java.time.*;
 import java.util.*;
 import java.util.concurrent.atomic.*;
 import java.util.function.*;
@@ -108,4 +109,8 @@ public class ResettableSupplier<T> implements Supplier<T> {
        public void reset() {
                cache.set(null);
        }
+
+       public void set(T value) {
+               cache.set(opt(value));
+       }
 }
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/time/GranularZonedDateTime.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/time/GranularZonedDateTime.java
index cf14142aef..0dc380462f 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/time/GranularZonedDateTime.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/time/GranularZonedDateTime.java
@@ -18,7 +18,6 @@ package org.apache.juneau.commons.time;
 
 import static org.apache.juneau.commons.utils.AssertionUtils.*;
 import static org.apache.juneau.commons.utils.StateEnum.*;
-import static org.apache.juneau.commons.utils.Utils.*;
 
 import java.time.*;
 import java.time.format.*;
@@ -164,15 +163,20 @@ public class GranularZonedDateTime {
         *      <li>With 4-9 fractional digits → {@link 
ChronoField#NANO_OF_SECOND}
         * </ul>
         *
-        * @param timestamp The ISO8601 timestamp string to parse.
+        * @param value The ISO8601 timestamp string to parse.
         * @return A new GranularZonedDateTime instance.
         * @throws IllegalArgumentException if timestamp is null.
         * @throws DateTimeParseException if the timestamp format is invalid.
         */
-       public static GranularZonedDateTime of(String timestamp) {
-               return of(timestamp, null);
+       public static GranularZonedDateTime of(String value) {
+               return of(value, null, null);
        }
 
+       public static GranularZonedDateTime of(String value, TimeProvider 
timeProvider) {
+               return of(value, null, timeProvider);
+       }
+
+
        /**
         * Parses an ISO8601 timestamp string into a GranularZonedDateTime with 
a default timezone.
         *
@@ -202,16 +206,17 @@ public class GranularZonedDateTime {
         *      <jc>// Result uses UTC (Z), not America/New_York</jc>
         * </p>
         *
-        * @param seg The ISO8601 timestamp string to parse.
+        * @param value The ISO8601 timestamp string to parse.
         * @param defaultZoneId The default timezone to use if no timezone is 
specified in the string.
         *      If null, {@link ZoneId#systemDefault()} is used.
         * @return A new GranularZonedDateTime instance.
         * @throws IllegalArgumentException if seg is null.
         * @throws DateTimeParseException if the timestamp format is invalid.
         */
-       public static GranularZonedDateTime of(String seg, ZoneId 
defaultZoneId) {
-               assertArgNotNull("seg", seg);
+       public static GranularZonedDateTime of(String value, ZoneId 
defaultZoneId, TimeProvider timeProvider) {
+               assertArgNotNull("value", value);
                var digit = StringUtils.DIGIT;
+               timeProvider = timeProvider == null ? TimeProvider.INSTANCE : 
timeProvider;
 
                // States:
                // S01: Looking for Y(S02) or T(S07).
@@ -244,8 +249,8 @@ public class GranularZonedDateTime {
                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);
+               for (var i = 0; i < value.length(); i++) {
+                       var c = value.charAt(i);
 
                        if (state == S1) {
                                // S01: Looking for Y(S02) or T(S07)
@@ -256,28 +261,28 @@ public class GranularZonedDateTime {
                                        timeOnly = true; // Mark as time-only 
format
                                        state = S7;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, 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);
+                                       year = parse(value, 4, mark, i, 0, 
9999);
                                        state = S3;
                                } else if (c == 'T') {
-                                       year = parse(seg, 4, mark, i, 0, 9999);
+                                       year = parse(value, 4, mark, i, 0, 
9999);
                                        state = S7;
                                } else if (c == 'Z') {
                                        zoneId = ZoneId.of("Z");
-                                       year = parse(seg, 4, mark, i, 0, 9999);
+                                       year = parse(value, 4, mark, i, 0, 
9999);
                                        state = S15;
                                } else if (c == '+') {
-                                       year = parse(seg, 4, mark, i, 0, 9999);
+                                       year = parse(value, 4, mark, i, 0, 
9999);
                                        nego = false;
                                        state = S16;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, i);
                                }
                        } else if (state == S3) {
                                // S03: Found -, looking for M(S04)
@@ -286,28 +291,28 @@ public class GranularZonedDateTime {
                                        state = S4;
                                        precision = ChronoField.MONTH_OF_YEAR;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, 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);
+                                       month = parse(value, 2, mark, i, 1, 12);
                                        state = S5;
                                } else if (c == 'T') {
-                                       month = parse(seg, 2, mark, i, 1, 12);
+                                       month = parse(value, 2, mark, i, 1, 12);
                                        state = S7;
                                } else if (c == 'Z') {
-                                       month = parse(seg, 2, mark, i, 1, 12);
+                                       month = parse(value, 2, mark, i, 1, 12);
                                        zoneId = ZoneId.of("Z");
                                        state = S15;
                                } else if (c == '+') {
-                                       month = parse(seg, 2, mark, i, 1, 12);
+                                       month = parse(value, 2, mark, i, 1, 12);
                                        nego = false;
                                        state = S16;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, i);
                                }
                        } else if (state == S5) {
                                // S05: Found -, looking for D(S06)
@@ -316,29 +321,29 @@ public class GranularZonedDateTime {
                                        state = S6;
                                        precision = ChronoField.DAY_OF_MONTH;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, 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);
+                                       day = parse(value, 2, mark, i, 1, 31);
                                        state = S7;
                                } else if (c == 'Z') {
-                                       day = parse(seg, 2, mark, i, 1, 31);
+                                       day = parse(value, 2, mark, i, 1, 31);
                                        zoneId = ZoneId.of("Z");
                                        state = S15;
                                } else if (c == '+') {
-                                       day = parse(seg, 2, mark, i, 1, 31);
+                                       day = parse(value, 2, mark, i, 1, 31);
                                        nego = false;
                                        state = S16;
                                } else if (c == '-') {
-                                       day = parse(seg, 2, mark, i, 1, 31);
+                                       day = parse(value, 2, mark, i, 1, 31);
                                        nego = true;
                                        state = S17;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, i);
                                }
                        } else if (state == S7) {
                                // S07: Found T, looking for 
h(S08)/Z(S15)/+(S16)/-(S17)
@@ -348,37 +353,46 @@ public class GranularZonedDateTime {
                                        precision = ChronoField.HOUR_OF_DAY;
                                } else if (c == 'Z') {
                                        zoneId = ZoneId.of("Z");
+                                       if (timeOnly) {
+                                               precision = 
ChronoField.HOUR_OF_DAY;
+                                       }
                                        state = S15;
                                } else if (c == '+') {
                                        nego = false;
+                                       if (timeOnly) {
+                                               precision = 
ChronoField.HOUR_OF_DAY;
+                                       }
                                        state = S16;
                                } else if (c == '-') {
                                        nego = true;
+                                       if (timeOnly) {
+                                               precision = 
ChronoField.HOUR_OF_DAY;
+                                       }
                                        state = S17;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, 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);
+                                       hour = parse(value, 2, mark, i, 0, 23);
                                        state = S9;
                                } else if (c == 'Z') {
-                                       hour = parse(seg, 2, mark, i, 0, 23);
+                                       hour = parse(value, 2, mark, i, 0, 23);
                                        zoneId = ZoneId.of("Z");
                                        state = S15;
                                } else if (c == '+') {
-                                       hour = parse(seg, 2, mark, i, 0, 23);
+                                       hour = parse(value, 2, mark, i, 0, 23);
                                        nego = false;
                                        state = S16;
                                } else if (c == '-') {
-                                       hour = parse(seg, 2, mark, i, 0, 23);
+                                       hour = parse(value, 2, mark, i, 0, 23);
                                        nego = true;
                                        state = S17;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, i);
                                }
                        } else if (state == S9) {
                                // S09: Found :, looking for m(S10)
@@ -387,29 +401,29 @@ public class GranularZonedDateTime {
                                        state = S10;
                                        precision = ChronoField.MINUTE_OF_HOUR;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, 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);
+                                       minute = parse(value, 2, mark, i, 0, 
59);
                                        state = S11;
                                } else if (c == 'Z') {
-                                       minute = parse(seg, 2, mark, i, 0, 59);
+                                       minute = parse(value, 2, mark, i, 0, 
59);
                                        zoneId = ZoneId.of("Z");
                                        state = S15;
                                } else if (c == '+') {
-                                       minute = parse(seg, 2, mark, i, 0, 59);
+                                       minute = parse(value, 2, mark, i, 0, 
59);
                                        nego = false;
                                        state = S16;
                                } else if (c == '-') {
-                                       minute = parse(seg, 2, mark, i, 0, 59);
+                                       minute = parse(value, 2, mark, i, 0, 
59);
                                        nego = true;
                                        state = S17;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, i);
                                }
                        } else if (state == S11) {
                                // S11: Found :, looking for s(S12)
@@ -418,30 +432,30 @@ public class GranularZonedDateTime {
                                        state = S12;
                                        precision = 
ChronoField.SECOND_OF_MINUTE;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, 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);
+                               second = parse(value, 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);
+                                       second = parse(value, 2, mark, i, 0, 
59);
                                        zoneId = ZoneId.of("Z");
                                        state = S15;
                                } else if (c == '+') {
-                                       second = parse(seg, 2, mark, i, 0, 59);
+                                       second = parse(value, 2, mark, i, 0, 
59);
                                        nego = false;
                                        state = S16;
                                } else if (c == '-') {
-                                       second = parse(seg, 2, mark, i, 0, 59);
+                                       second = parse(value, 2, mark, i, 0, 
59);
                                        nego = true;
                                        state = S17;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, i);
                                }
                        } else if (state == S13) {
                                // S13: Found . or ,, looking for 
S(S14)/Z(S15)/+(S16)/-(S17)
@@ -458,46 +472,46 @@ public class GranularZonedDateTime {
                                        nego = true;
                                        state = S17;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, 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);
+                                       nanos = parseNanos(value, 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);
+                                       nanos = parseNanos(value, 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);
+                                       nanos = parseNanos(value, 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);
+                                       throw bad(value, i);
                                }
                        } else if (state == S15) {
                                // Shouldn't find anything after Z
-                               throw bad(seg, i);
+                               throw bad(value, i);
                        } else if (state == S16) {
                                // S16: Found +, looking for oh(S18)
                                if (digit.contains(c)) {
                                        mark = i;
                                        state = S18;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, i);
                                }
                        } else if (state == S17) {
                                // S17: Found -, looking for oh(S18)
@@ -505,17 +519,17 @@ public class GranularZonedDateTime {
                                        mark = i;
                                        state = S18;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, 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);
+                                       ohour = parse(value, 2, mark, i, 0, 18);
                                        state = S19;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, i);
                                }
                                // If we reach end of string, ohour is complete 
(2 digits)
                        } else if (state == S19) {
@@ -524,48 +538,48 @@ public class GranularZonedDateTime {
                                        mark = i;
                                        state = S20;
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, i);
                                }
                        } else /* (state == S20) */ {
                                // S20: Found om, looking for om(S20)
                                if (digit.contains(c)) {
                                        // Stay in S20
                                } else {
-                                       throw bad(seg, i);
+                                       throw bad(value, i);
                                }
                        }
                }
 
-               var end = seg.length(); // end is exclusive (one past last 
character)
+               var end = value.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);
+                       throw bad(value, end - 1);
                } else if (state == S2) {
                        // S02: Found Y, looking for Y(S02)/-(S03)/T(S07).
-                       year = parse(seg, 4, mark, end, 0, 9999);
+                       year = parse(value, 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);
+                       month = parse(value, 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);
+                       day = parse(value, 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);
+                       hour = parse(value, 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);
+                       minute = parse(value, 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);
+                       second = parse(value, 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);
+                       nanos = parseNanos(value, 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;
@@ -575,17 +589,17 @@ public class GranularZonedDateTime {
                        // 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);
+                               ohour = parse(value, 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);
+                               ohour = parse(value, 2, mark, mark + 2, 0, 18);
+                               ominute = parse(value, 2, mark + 2, end, 0, 59);
                        } else {
-                               throw bad(seg, mark);
+                               throw bad(value, mark);
                        }
                } else /* (state == S20) */ {
                        // S20: Found om, looking for om(S20).
-                       ominute = parse(seg, 2, mark, end, 0, 59);
+                       ominute = parse(value, 2, mark, end, 0, 59);
                }
 
                // Build ZoneId if we have offset information
@@ -604,7 +618,7 @@ public class GranularZonedDateTime {
 
                // Use provided default zone if no zone specified, otherwise 
use system default
                if (zoneId == null) {
-                       zoneId = defaultZoneId != null ? defaultZoneId : 
ZoneId.systemDefault();
+                       zoneId = defaultZoneId != null ? defaultZoneId : 
timeProvider.getSystemDefaultZoneId();
                }
 
                // Construct ZonedDateTime from parsed values
@@ -613,7 +627,7 @@ public class GranularZonedDateTime {
                // For date formats, default to 1/1/1
                if (timeOnly) {
                        // Time-only format: use current year/month/day
-                       var now = ZonedDateTime.now(zoneId);
+                       var now = timeProvider.now(zoneId);
                        year = now.getYear();
                        month = now.getMonthValue();
                        day = now.getDayOfMonth();
@@ -684,6 +698,19 @@ public class GranularZonedDateTime {
         * This method provides a mapping from date/time fields to time units.
         * Not all ChronoField values have direct ChronoUnit equivalents.
         *
+        * <p>
+        * Supported fields:
+        * <ul>
+        *      <li>{@link ChronoField#YEAR} → {@link ChronoUnit#YEARS}
+        *      <li>{@link ChronoField#MONTH_OF_YEAR} → {@link 
ChronoUnit#MONTHS}
+        *      <li>{@link ChronoField#DAY_OF_MONTH} → {@link ChronoUnit#DAYS}
+        *      <li>{@link ChronoField#HOUR_OF_DAY} → {@link ChronoUnit#HOURS}
+        *      <li>{@link ChronoField#MINUTE_OF_HOUR} → {@link 
ChronoUnit#MINUTES}
+        *      <li>{@link ChronoField#SECOND_OF_MINUTE} → {@link 
ChronoUnit#SECONDS}
+        *      <li>{@link ChronoField#MILLI_OF_SECOND} → {@link 
ChronoUnit#MILLIS}
+        *      <li>{@link ChronoField#NANO_OF_SECOND} → {@link 
ChronoUnit#NANOS}
+        * </ul>
+        *
         * @param field The ChronoField to convert
         * @return The corresponding ChronoUnit, or null if no direct mapping 
exists
         */
@@ -696,6 +723,7 @@ public class GranularZonedDateTime {
                        case MINUTE_OF_HOUR -> ChronoUnit.MINUTES;
                        case SECOND_OF_MINUTE -> ChronoUnit.SECONDS;
                        case MILLI_OF_SECOND -> ChronoUnit.MILLIS;
+                       case NANO_OF_SECOND -> ChronoUnit.NANOS;
                        default -> null;
                };
        }
@@ -733,6 +761,32 @@ public class GranularZonedDateTime {
         */
        public ZonedDateTime getZonedDateTime() { return zdt; }
 
+       /**
+        * Returns the precision of this time value.
+        *
+        * <p>
+        * The precision indicates the finest granularity of the time value, 
which determines
+        * how the value was parsed or created. For example:
+        * <ul>
+        *      <li>{@link ChronoField#YEAR} - Year precision (e.g., "2011")
+        *      <li>{@link ChronoField#MONTH_OF_YEAR} - Month precision (e.g., 
"2011-01")
+        *      <li>{@link ChronoField#DAY_OF_MONTH} - Day precision (e.g., 
"2011-01-15")
+        *      <li>{@link ChronoField#HOUR_OF_DAY} - Hour precision (e.g., 
"2011-01-15T12")
+        *      <li>{@link ChronoField#MINUTE_OF_HOUR} - Minute precision 
(e.g., "2011-01-15T12:30")
+        *      <li>{@link ChronoField#SECOND_OF_MINUTE} - Second precision 
(e.g., "2011-01-15T12:30:45")
+        *      <li>{@link ChronoField#MILLI_OF_SECOND} - Millisecond precision 
(e.g., "2011-01-15T12:30:45.123")
+        *      <li>{@link ChronoField#NANO_OF_SECOND} - Nanosecond precision 
(e.g., "2011-01-15T12:30:45.123456789")
+        * </ul>
+        *
+        * @return The precision of this time value.
+        */
+       public ChronoField getPrecision() { return precision; }
+
+       @Override
+       public String toString() {
+               return zdt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + "(" 
+ precision + ")";
+       }
+
        /**
         * Rolls this time value by the specified amount using the specified 
field.
         *
@@ -740,6 +794,18 @@ public class GranularZonedDateTime {
         * This method creates a new GranularZonedDateTime by adding the 
specified amount to the
         * specified field. The precision of the returned object remains the 
same as this object.
         *
+        * <h5 class='section'>Supported Fields:</h5>
+        * <ul>
+        *      <li>{@link ChronoField#YEAR}
+        *      <li>{@link ChronoField#MONTH_OF_YEAR}
+        *      <li>{@link ChronoField#DAY_OF_MONTH}
+        *      <li>{@link ChronoField#HOUR_OF_DAY}
+        *      <li>{@link ChronoField#MINUTE_OF_HOUR}
+        *      <li>{@link ChronoField#SECOND_OF_MINUTE}
+        *      <li>{@link ChronoField#MILLI_OF_SECOND}
+        *      <li>{@link ChronoField#NANO_OF_SECOND}
+        * </ul>
+        *
         * <h5 class='section'>Example:</h5>
         * <p class='bjava'>
         *      <jc>// Create a datetime with hour precision</jc>
@@ -749,21 +815,16 @@ public class GranularZonedDateTime {
         *      <jc>// Result: 2011-01-15T14:00:00Z (precision still 
HOUR_OF_DAY)</jc>
         * </p>
         *
-        * <p>
-        * If the field cannot be converted to a ChronoUnit (e.g., unsupported 
field), this method
-        * returns this object unchanged.
-        *
-        * @param field The field to roll by (e.g., {@link ChronoField#YEAR}, 
{@link ChronoField#HOUR_OF_DAY}).
+        * @param field The field to roll by. Must be one of the supported 
fields listed above.
         * @param amount The amount to roll by. Positive values roll forward, 
negative values roll backward.
-        * @return A new GranularZonedDateTime with the rolled value, or this 
object if the field is unsupported.
+        * @return A new GranularZonedDateTime with the rolled value.
+        * @throws IllegalArgumentException If the field is not supported.
         */
        public GranularZonedDateTime roll(ChronoField field, int amount) {
-               ChronoUnit unit = toChronoUnit(field);
-               if (nn(unit)) {
-                       ZonedDateTime newZdt = zdt.plus(amount, unit);
-                       return new GranularZonedDateTime(newZdt, precision);
-               }
-               return this;
+               var unit = toChronoUnit(field);
+               assertArg(unit != null, "Unsupported roll field: {0}", field);
+               var newZdt = zdt.plus(amount, unit);
+               return new GranularZonedDateTime(newZdt, precision);
        }
 
        /**
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/time/TimeProvider.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/time/TimeProvider.java
new file mode 100644
index 0000000000..fd76196d3d
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/time/TimeProvider.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.time;
+
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.time.*;
+import java.util.function.*;
+
+import org.apache.juneau.commons.function.*;
+
+public class TimeProvider {
+
+       public static final TimeProvider INSTANCE = new TimeProvider();
+
+       public ZoneId getSystemDefaultZoneId() {
+               return ZoneId.systemDefault();
+       }
+
+       public ZonedDateTime now() {
+               return ZonedDateTime.now();
+       }
+
+       public ZonedDateTime now(ZoneId zoneId) {
+               return ZonedDateTime.now(zoneId);
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/time/GranularZonedDateTime_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/time/GranularZonedDateTime_Test.java
index cc2969ee18..6afb53d1b5 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/time/GranularZonedDateTime_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/time/GranularZonedDateTime_Test.java
@@ -16,15 +16,18 @@
  */
 package org.apache.juneau.commons.time;
 
-import static org.apache.juneau.TestUtils.assertThrowsWithMessage;
+import static org.apache.juneau.TestUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.time.*;
 import java.time.temporal.*;
-import java.util.*;
+import java.util.Date;
 
 import org.apache.juneau.*;
+import org.apache.juneau.utest.utils.FakeTimeProvider;
 import org.junit.jupiter.api.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
 
 /**
  * Tests for {@link GranularZonedDateTime}.
@@ -46,169 +49,76 @@ class GranularZonedDateTime_Test extends TestBase {
        @Test
        void b02_constructorWithZonedDateTime_preservesZone() {
                var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("America/New_York"));
-               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.MINUTE_OF_HOUR);
+               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.HOUR_OF_DAY);
                assertEquals(ZoneId.of("America/New_York"), gdt.zdt.getZone());
        }
 
        @Test
-       void b03_constructorWithZonedDateTime_allPrecisions() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 123000000, 
ZoneId.of("UTC"));
-               var precisions = new ChronoField[] {
-                       ChronoField.YEAR,
-                       ChronoField.MONTH_OF_YEAR,
-                       ChronoField.DAY_OF_MONTH,
-                       ChronoField.HOUR_OF_DAY,
-                       ChronoField.MINUTE_OF_HOUR,
-                       ChronoField.SECOND_OF_MINUTE,
-                       ChronoField.MILLI_OF_SECOND
-               };
-               for (var precision : precisions) {
-                       var gdt = new GranularZonedDateTime(zdt, precision);
-                       assertEquals(zdt, gdt.zdt);
-                       assertEquals(precision, gdt.precision);
-               }
-       }
-
-       
//====================================================================================================
-       // copy() tests
-       
//====================================================================================================
-
-       @Test
-       void c01_copy_createsNewInstance() {
+       void b03_constructorWithZonedDateTime_preservesPrecision() {
                var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt1 = new GranularZonedDateTime(zdt, 
ChronoField.DAY_OF_MONTH);
-               var gdt2 = gdt1.copy();
-               assertNotSame(gdt1, gdt2);
-               assertEquals(gdt1.zdt, gdt2.zdt);
-               assertEquals(gdt1.precision, gdt2.precision);
+               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.MINUTE_OF_HOUR);
+               assertEquals(ChronoField.MINUTE_OF_HOUR, gdt.precision);
        }
 
        @Test
-       void c02_copy_immutable() {
+       void b04_ofZonedDateTime() {
                var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt1 = new GranularZonedDateTime(zdt, ChronoField.YEAR);
-               var gdt2 = gdt1.copy();
-               // Modifying the copy should not affect original (though 
ZonedDateTime is immutable anyway)
-               var gdt3 = gdt2.roll(1);
-               assertEquals(gdt1.zdt, gdt1.zdt); // Original unchanged
-               assertNotEquals(gdt1.zdt, gdt3.zdt);
+               var gdt = GranularZonedDateTime.of(zdt, 
ChronoField.HOUR_OF_DAY);
+               assertEquals(zdt, gdt.zdt);
+               assertEquals(ChronoField.HOUR_OF_DAY, gdt.precision);
        }
 
-       
//====================================================================================================
-       // getZonedDateTime() tests
-       
//====================================================================================================
-
        @Test
-       void d01_getZonedDateTime_returnsCorrectValue() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.HOUR_OF_DAY);
-               assertEquals(zdt, gdt.getZonedDateTime());
-               assertSame(gdt.zdt, gdt.getZonedDateTime());
+       void b05_ofDate() {
+               var date = new Date(1705322445000L); // 2024-01-15T12:30:45Z
+               var gdt = GranularZonedDateTime.of(date, 
ChronoField.SECOND_OF_MINUTE);
+               assertEquals(ChronoField.SECOND_OF_MINUTE, gdt.precision);
+               // Verify the instant is correct (date uses system default 
timezone)
+               assertEquals(date.toInstant(), gdt.zdt.toInstant());
        }
 
        @Test
-       void d02_getZonedDateTime_differentZones() {
-               var zdt1 = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var zdt2 = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("America/New_York"));
-               var gdt1 = new GranularZonedDateTime(zdt1, 
ChronoField.MINUTE_OF_HOUR);
-               var gdt2 = new GranularZonedDateTime(zdt2, 
ChronoField.MINUTE_OF_HOUR);
-               assertEquals(zdt1, gdt1.getZonedDateTime());
-               assertEquals(zdt2, gdt2.getZonedDateTime());
-               assertNotEquals(gdt1.getZonedDateTime(), 
gdt2.getZonedDateTime());
+       void b06_ofDateWithZoneId() {
+               var date = new Date(1705322445000L); // 2024-01-15T12:30:45Z
+               var zoneId = ZoneId.of("America/New_York");
+               var gdt = GranularZonedDateTime.of(date, 
ChronoField.SECOND_OF_MINUTE, zoneId);
+               assertEquals(ChronoField.SECOND_OF_MINUTE, gdt.precision);
+               assertEquals(zoneId, gdt.zdt.getZone());
+               assertEquals(2024, gdt.zdt.getYear());
+               assertEquals(1, gdt.zdt.getMonthValue());
+               assertEquals(15, gdt.zdt.getDayOfMonth());
        }
 
        
//====================================================================================================
-       // roll(int) tests
+       // copy() tests
        
//====================================================================================================
 
        @Test
-       void e01_roll_forwardByOne() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, ChronoField.YEAR);
-               var rolled = gdt.roll(1);
-               assertEquals(zdt.plusYears(1), rolled.zdt);
-               assertEquals(ChronoField.YEAR, rolled.precision);
-       }
-
-       @Test
-       void e02_roll_backwardByOne() {
+       void c01_copy() {
                var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, ChronoField.YEAR);
-               var rolled = gdt.roll(-1);
-               assertEquals(zdt.minusYears(1), rolled.zdt);
-               assertEquals(ChronoField.YEAR, rolled.precision);
-       }
-
-       @Test
-       void e03_roll_byMultiple() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.MONTH_OF_YEAR);
-               var rolled = gdt.roll(3);
-               assertEquals(zdt.plusMonths(3), rolled.zdt);
-       }
-
-       @Test
-       void e04_roll_monthPrecision() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.MONTH_OF_YEAR);
-               var rolled = gdt.roll(1);
-               assertEquals(zdt.plusMonths(1), rolled.zdt);
+               var gdt1 = new GranularZonedDateTime(zdt, 
ChronoField.HOUR_OF_DAY);
+               var gdt2 = gdt1.copy();
+               assertNotSame(gdt1, gdt2);
+               assertEquals(gdt1.zdt, gdt2.zdt);
+               assertEquals(gdt1.precision, gdt2.precision);
        }
 
-       @Test
-       void e05_roll_dayPrecision() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.DAY_OF_MONTH);
-               var rolled = gdt.roll(1);
-               assertEquals(zdt.plusDays(1), rolled.zdt);
-       }
+       
//====================================================================================================
+       // getZonedDateTime() tests
+       
//====================================================================================================
 
        @Test
-       void e06_roll_hourPrecision() {
+       void d01_getZonedDateTime() {
                var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
                var gdt = new GranularZonedDateTime(zdt, 
ChronoField.HOUR_OF_DAY);
-               var rolled = gdt.roll(1);
-               assertEquals(zdt.plusHours(1), rolled.zdt);
+               assertEquals(zdt, gdt.getZonedDateTime());
        }
 
        @Test
-       void e07_roll_minutePrecision() {
+       void d02_getPrecision() {
                var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
                var gdt = new GranularZonedDateTime(zdt, 
ChronoField.MINUTE_OF_HOUR);
-               var rolled = gdt.roll(1);
-               assertEquals(zdt.plusMinutes(1), rolled.zdt);
-       }
-
-       @Test
-       void e08_roll_secondPrecision() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.SECOND_OF_MINUTE);
-               var rolled = gdt.roll(1);
-               assertEquals(zdt.plusSeconds(1), rolled.zdt);
-       }
-
-       @Test
-       void e09_roll_millisecondPrecision() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 123000000, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.MILLI_OF_SECOND);
-               var rolled = gdt.roll(1);
-               assertEquals(zdt.plus(1, ChronoUnit.MILLIS), rolled.zdt);
-       }
-
-       @Test
-       void e10_roll_zero() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, ChronoField.YEAR);
-               var rolled = gdt.roll(0);
-               assertEquals(zdt, rolled.zdt);
-       }
-
-       @Test
-       void e11_roll_preservesPrecision() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.DAY_OF_MONTH);
-               var rolled = gdt.roll(5);
-               assertEquals(ChronoField.DAY_OF_MONTH, rolled.precision);
+               assertEquals(ChronoField.MINUTE_OF_HOUR, gdt.getPrecision());
        }
 
        
//====================================================================================================
@@ -216,26 +126,7 @@ class GranularZonedDateTime_Test extends TestBase {
        
//====================================================================================================
 
        @Test
-       void f01_rollWithField_differentField() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, ChronoField.YEAR);
-               var rolled = gdt.roll(ChronoField.MONTH_OF_YEAR, 2);
-               assertEquals(zdt.plusMonths(2), rolled.zdt);
-               // Precision should remain the same as original
-               assertEquals(ChronoField.YEAR, rolled.precision);
-       }
-
-       @Test
-       void f02_rollWithField_sameField() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, ChronoField.YEAR);
-               var rolled = gdt.roll(ChronoField.YEAR, 1);
-               assertEquals(zdt.plusYears(1), rolled.zdt);
-               assertEquals(ChronoField.YEAR, rolled.precision);
-       }
-
-       @Test
-       void f03_rollWithField_allSupportedFields() {
+       void e01_roll_withField() {
                var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 123000000, 
ZoneId.of("UTC"));
                var gdt = new GranularZonedDateTime(zdt, 
ChronoField.MILLI_OF_SECOND);
 
@@ -259,150 +150,42 @@ class GranularZonedDateTime_Test extends TestBase {
 
                var rolled7 = gdt.roll(ChronoField.MILLI_OF_SECOND, 1);
                assertEquals(zdt.plus(1, ChronoUnit.MILLIS), rolled7.zdt);
-       }
 
-       @Test
-       void f04_rollWithField_unsupportedField() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, ChronoField.YEAR);
-               // Using an unsupported ChronoField (one that toChronoUnit 
returns null for)
-               var rolled = gdt.roll(ChronoField.DAY_OF_WEEK, 1);
-               // Should return the same instance (no change)
-               assertEquals(gdt.zdt, rolled.zdt);
-               assertEquals(gdt.precision, rolled.precision);
+               var rolled8 = gdt.roll(ChronoField.NANO_OF_SECOND, 1);
+               assertEquals(zdt.plus(1, ChronoUnit.NANOS), rolled8.zdt);
        }
 
        @Test
-       void f05_rollWithField_negativeAmount() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, ChronoField.YEAR);
-               var rolled = gdt.roll(ChronoField.MONTH_OF_YEAR, -3);
-               assertEquals(zdt.minusMonths(3), rolled.zdt);
-       }
+       void e02_roll_withNanoOfSecondPrecision() {
+               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 123456789, 
ZoneId.of("UTC"));
+               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.NANO_OF_SECOND);
 
-       @Test
-       void f06_rollWithField_zeroAmount() {
-               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, ChronoField.YEAR);
-               var rolled = gdt.roll(ChronoField.MONTH_OF_YEAR, 0);
-               assertEquals(zdt, rolled.zdt);
+               var rolled = gdt.roll(ChronoField.NANO_OF_SECOND, 100);
+               assertEquals(zdt.plus(100, ChronoUnit.NANOS), rolled.zdt);
+               assertEquals(ChronoField.NANO_OF_SECOND, rolled.precision);
        }
 
        @Test
-       void f07_rollWithField_preservesOriginalPrecision() {
+       void e03_roll_withUnsupportedField() {
                var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
-               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.DAY_OF_MONTH);
-               var rolled = gdt.roll(ChronoField.YEAR, 1);
-               // Precision should remain DAY_OF_MONTH, not YEAR
-               assertEquals(ChronoField.DAY_OF_MONTH, rolled.precision);
+               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.HOUR_OF_DAY);
+
+               assertThrowsWithMessage(IllegalArgumentException.class,
+                       "Unsupported roll field: AmPmOfDay",
+                       () -> gdt.roll(ChronoField.AMPM_OF_DAY, 1));
        }
 
        
//====================================================================================================
-       // of(String) tests
+       // roll(int) tests
        
//====================================================================================================
 
        @Test
-       void g01_of_yearOnly() {
-               var gdt = GranularZonedDateTime.of("2011");
-               assertNotNull(gdt);
-               assertEquals(2011, gdt.zdt.getYear());
-               assertEquals(ChronoField.YEAR, gdt.precision);
-       }
-
-       @Test
-       void g02_of_yearMonth() {
-               var gdt = GranularZonedDateTime.of("2011-01");
-               assertNotNull(gdt);
-               assertEquals(2011, gdt.zdt.getYear());
-               assertEquals(1, gdt.zdt.getMonthValue());
-               assertEquals(ChronoField.MONTH_OF_YEAR, gdt.precision);
-       }
-
-       @Test
-       void g03_of_date() {
-               var gdt = GranularZonedDateTime.of("2011-01-15");
-               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 g04_of_dateTime_hour() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12Z");
-               assertNotNull(gdt);
-               assertEquals(12, gdt.zdt.getHour());
-               assertEquals(ChronoField.HOUR_OF_DAY, gdt.precision);
-       }
-
-       @Test
-       void g05_of_dateTime_minute() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30Z");
-               assertNotNull(gdt);
-               assertEquals(12, gdt.zdt.getHour());
-               assertEquals(30, gdt.zdt.getMinute());
-               assertEquals(ChronoField.MINUTE_OF_HOUR, gdt.precision);
-       }
-
-       @Test
-       void g06_of_dateTime_second() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45Z");
-               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 g07_of_dateTime_millisecond() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45.123Z");
-               assertNotNull(gdt);
-               assertEquals(12, gdt.zdt.getHour());
-               assertEquals(30, gdt.zdt.getMinute());
-               assertEquals(45, gdt.zdt.getSecond());
-               assertEquals(123, gdt.zdt.getNano() / 1_000_000);
-               assertEquals(ChronoField.MILLI_OF_SECOND, gdt.precision);
-       }
-
-       @Test
-       void g08_of_withOffset() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45-05:00");
-               assertNotNull(gdt);
-               assertEquals(12, gdt.zdt.getHour());
-               assertEquals(ZoneOffset.of("-05:00"), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void g09_of_withPositiveOffset() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45+09:00");
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.of("+09:00"), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void g10_of_invalidString() {
-               // fromIso8601 throws DateTimeParseException for invalid input
-               // The exception may be DateTimeParseException (from 
fromIso8601) or BasicRuntimeException (from of)
-               var e = assertThrows(RuntimeException.class, () -> {
-                       GranularZonedDateTime.of("invalid-date");
-               });
-               // Verify that an exception was thrown (the exact message 
format may vary)
-               assertNotNull(e);
-               assertNotNull(e.getMessage());
-       }
-
-       @Test
-       void g11_of_null() {
-               assertThrows(IllegalArgumentException.class, () -> 
GranularZonedDateTime.of(null));
-       }
-
-       @Test
-       void g12_of_emptyString() {
-               assertThrowsWithMessage(RuntimeException.class, "Invalid 
ISO8601 timestamp", () -> {
-                       GranularZonedDateTime.of("");
-               });
+       void f01_roll_withPrecision() {
+               var zdt = ZonedDateTime.of(2024, 1, 15, 12, 30, 45, 0, 
ZoneId.of("UTC"));
+               var gdt = new GranularZonedDateTime(zdt, 
ChronoField.HOUR_OF_DAY);
+               var rolled = gdt.roll(2);
+               assertEquals(zdt.plusHours(2), rolled.zdt);
+               assertEquals(ChronoField.HOUR_OF_DAY, rolled.precision);
        }
 
        
//====================================================================================================
@@ -411,7 +194,7 @@ class GranularZonedDateTime_Test extends TestBase {
 
        @Test
        void h01_rollAndof() {
-               var gdt1 = GranularZonedDateTime.of("2011");
+               var gdt1 = GranularZonedDateTime.of("2011", FAKE_TIME_PROVIDER);
                var gdt2 = gdt1.roll(1);
                assertEquals(2012, gdt2.zdt.getYear());
                assertEquals(ChronoField.YEAR, gdt2.precision);
@@ -419,7 +202,7 @@ class GranularZonedDateTime_Test extends TestBase {
 
        @Test
        void h02_rollMultipleTimes() {
-               var gdt = GranularZonedDateTime.of("2011-01-15");
+               var gdt = GranularZonedDateTime.of("2011-01-15", 
FAKE_TIME_PROVIDER);
                var rolled1 = gdt.roll(1);
                var rolled2 = rolled1.roll(1);
                assertEquals(17, rolled2.zdt.getDayOfMonth());
@@ -427,7 +210,7 @@ class GranularZonedDateTime_Test extends TestBase {
 
        @Test
        void h03_rollWithDifferentField() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30Z");
+               var gdt = GranularZonedDateTime.of("2011-01-15T12:30Z", 
FAKE_TIME_PROVIDER);
                // Roll by hours even though precision is minutes
                var rolled = gdt.roll(ChronoField.HOUR_OF_DAY, 2);
                assertEquals(14, rolled.zdt.getHour());
@@ -437,7 +220,7 @@ class GranularZonedDateTime_Test extends TestBase {
 
        @Test
        void h04_copyAndRoll() {
-               var gdt1 = GranularZonedDateTime.of("2011-01-15");
+               var gdt1 = GranularZonedDateTime.of("2011-01-15", 
FAKE_TIME_PROVIDER);
                var gdt2 = gdt1.copy();
                var rolled = gdt2.roll(1);
                // Original should be unchanged
@@ -446,1081 +229,245 @@ class GranularZonedDateTime_Test extends TestBase {
        }
 
        
//====================================================================================================
-       // of(String) tests
-       
//====================================================================================================
-
-       @Test
-       void i01_of_null() {
-               // of(String) overload throws IllegalArgumentException when seg 
is null
-               assertThrows(IllegalArgumentException.class, () -> {
-                       GranularZonedDateTime.of((String)null);
-               });
-               // of(String, ZoneId) also throws when seg is null
-               assertThrows(IllegalArgumentException.class, () -> {
-                       GranularZonedDateTime.of((String)null, (ZoneId)null);
-               });
-       }
-
-       @Test
-       void i02_of_yearOnly() {
-               var gdt = GranularZonedDateTime.of("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_of_yearMonth() {
-               var gdt = GranularZonedDateTime.of("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_of_date() {
-               var gdt = GranularZonedDateTime.of("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_of_dateTime_hour() {
-               var gdt = GranularZonedDateTime.of("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_of_dateTime_minute() {
-               var gdt = GranularZonedDateTime.of("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_of_dateTime_second() {
-               var gdt = GranularZonedDateTime.of("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_of_dateTime_millisecond_dot() {
-               var gdt = GranularZonedDateTime.of("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_of_dateTime_millisecond_comma() {
-               var gdt = GranularZonedDateTime.of("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_of_timeOnly_hour() {
-               var now = ZonedDateTime.now();
-               var gdt = GranularZonedDateTime.of("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_of_timeOnly_minute() {
-               var now = ZonedDateTime.now();
-               var gdt = GranularZonedDateTime.of("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_of_timeOnly_second() {
-               var now = ZonedDateTime.now();
-               var gdt = GranularZonedDateTime.of("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_of_timeOnly_millisecond() {
-               var now = ZonedDateTime.now();
-               var gdt = GranularZonedDateTime.of("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_of_withZ() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45Z", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-               assertEquals(12, gdt.zdt.getHour());
-       }
-
-       @Test
-       void i15_of_withOffset_plusHH() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45+05", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i16_of_withOffset_minusHH() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45-05", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i17_of_withOffset_plusHHMM() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45+0530", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHoursMinutes(5, 30), 
gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i18_of_withOffset_minusHHMM() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45-0530", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHoursMinutes(-5, -30), 
gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i19_of_withOffset_plusHH_MM() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45+05:30", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHoursMinutes(5, 30), 
gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i20_of_withOffset_minusHH_MM() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45-05:30", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHoursMinutes(-5, -30), 
gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i21_of_timezoneAfterYear() {
-               var gdt = GranularZonedDateTime.of("2011Z", null);
-               assertNotNull(gdt);
-               assertEquals(2011, gdt.zdt.getYear());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i22_of_timezoneAfterMonth() {
-               var gdt = GranularZonedDateTime.of("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_of_timezoneAfterDay() {
-               var gdt = GranularZonedDateTime.of("2011-01-15Z", null);
-               assertNotNull(gdt);
-               assertEquals(15, gdt.zdt.getDayOfMonth());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i24_of_timezoneAfterHour() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12Z", null);
-               assertNotNull(gdt);
-               assertEquals(12, gdt.zdt.getHour());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i25_of_timezoneAfterMinute() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30Z", null);
-               assertNotNull(gdt);
-               assertEquals(30, gdt.zdt.getMinute());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i26_of_timezoneAfterT() {
-               var gdt = GranularZonedDateTime.of("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_of_nanoseconds_1digit() {
-               var gdt = GranularZonedDateTime.of("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_of_nanoseconds_2digits() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45.12", 
null);
-               assertNotNull(gdt);
-               assertEquals(120000000, gdt.zdt.getNano());
-       }
-
-       @Test
-       void i29_of_nanoseconds_3digits() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45.123", 
null);
-               assertNotNull(gdt);
-               assertEquals(123000000, gdt.zdt.getNano());
-       }
-
-       @Test
-       void i30_of_nanoseconds_4digits() {
-               // Lines 1018: len == 4
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45.1234", 
null);
-               assertNotNull(gdt);
-               assertEquals(123400000, gdt.zdt.getNano());
-               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
-       }
-
-       @Test
-       void i30a_of_nanoseconds_5digits() {
-               // Lines 1019: len == 5
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45.12345", 
null);
-               assertNotNull(gdt);
-               assertEquals(123450000, gdt.zdt.getNano());
-               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
-       }
-
-       @Test
-       void i30b_of_nanoseconds_6digits() {
-               // Lines 1020: len == 6
-               var gdt = 
GranularZonedDateTime.of("2011-01-15T12:30:45.123456", null);
-               assertNotNull(gdt);
-               assertEquals(123456000, gdt.zdt.getNano());
-               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
-       }
-
-       @Test
-       void i30c_of_nanoseconds_7digits() {
-               // Lines 1021: len == 7
-               var gdt = 
GranularZonedDateTime.of("2011-01-15T12:30:45.1234567", null);
-               assertNotNull(gdt);
-               assertEquals(123456700, gdt.zdt.getNano());
-               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
-       }
-
-       @Test
-       void i30d_of_nanoseconds_8digits() {
-               // Lines 1022: len == 8
-               var gdt = 
GranularZonedDateTime.of("2011-01-15T12:30:45.12345678", null);
-               assertNotNull(gdt);
-               assertEquals(123456780, gdt.zdt.getNano());
-               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
-       }
-
-       @Test
-       void i30e_of_nanoseconds_9digits() {
-               // Line 1023: len == 9 (default return)
-               var gdt = 
GranularZonedDateTime.of("2011-01-15T12:30:45.123456789", null);
-               assertNotNull(gdt);
-               assertEquals(123456789, gdt.zdt.getNano());
-               assertEquals(ChronoField.NANO_OF_SECOND, gdt.precision);
-       }
-
-       @Test
-       void i31_of_badTimestamps() {
-               // Invalid formats
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("invalid", null));
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("", null));
-
-               // Invalid year length
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("123", null));
-
-               // Invalid month - below minimum (0)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-00", null));
-
-               // Invalid month - above maximum (13)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-13", null));
-
-               // Invalid month - way above maximum
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-99", null));
-
-               // Invalid day - below minimum (0)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-00", null));
-
-               // Invalid day - above maximum (32)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-32", null));
-
-               // Invalid day - way above maximum
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("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.of("2011-01-15T24", null));
-
-               // Invalid hour - way above maximum
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T99", null));
-
-               // Invalid minute - above maximum (60)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:60", null));
-
-               // Invalid minute - way above maximum
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:99", null));
-
-               // Invalid second - above maximum (60)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30:60", null));
-
-               // Invalid second - way above maximum
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30:99", null));
-
-               // Invalid character after year (line 642)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011X", null));
-
-               // Invalid character after '-' in S3 (line 651)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-X", null));
-
-               // Invalid character after month (line 676)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01X", null));
-
-               // Invalid character after '-' in S5 (line 685)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-X", null));
-
-               // Invalid character after day (line 707)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15X", null));
-
-               // Invalid character after 'T' in S7 (line 725)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("TX", null));
-
-               // Invalid character after hour (line 747)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12X", null));
-
-               // Invalid character after ':' in S9 (line 756)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:X", null));
-
-               // Invalid character after minute (line 778)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30X", null));
-
-               // Invalid character after ':' in S11 (line 787)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30:X", null));
-
-               // Invalid character in S12 after seconds (line 810)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30:45X", null));
-
-               // Invalid character in S13 after '.' or ',' (line 827)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30:45.X", null));
-
-               // Invalid character in S14 while reading milliseconds (line 
846)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30:45.12X", null));
-
-               // Invalid character in S16 after '+' (line 857)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30:45+X", null));
-
-               // Invalid character in S17 after '-' (line 865)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30:45-X", null));
-
-               // Invalid character in S18 while reading offset hours (line 
875)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30:45+05X", null));
-
-               // Invalid character in S19 after ':' in offset (line 884)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("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.of("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.of("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.of("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.of("2011-01-15T12:30:45+12345", null));
-
-               // Invalid nanosecond length in ofNanos (line 1010)
-               // This is defensive code - ofNanos 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 ofNanos 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 ofNanos with 
len=10, which is >9, triggering line 1010.
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> GranularZonedDateTime.of("2011-01-15T12:30:45.1234567890", null));
-       }
-
-       @Test
-       void i38_of_timeOnly_withZ() {
-               var now = ZonedDateTime.now();
-               var gdt = GranularZonedDateTime.of("T12:30:45Z", null);
-               assertNotNull(gdt);
-               assertEquals(now.getYear(), gdt.zdt.getYear());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i39_of_timeOnly_withOffset() {
-               var now = ZonedDateTime.now();
-               var gdt = GranularZonedDateTime.of("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_of_noTimezone_usesSystemDefault() {
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45", null);
-               assertNotNull(gdt);
-               assertEquals(ZoneId.systemDefault(), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i41_of_yearFollowedByT() {
-               // Lines 627-628: Year followed by 'T'
-               var gdt = GranularZonedDateTime.of("2011T12", null);
-               assertNotNull(gdt);
-               assertEquals(2011, gdt.zdt.getYear());
-               assertEquals(12, gdt.zdt.getHour());
-       }
-
-       @Test
-       void i42_of_yearFollowedByZ() {
-               // Lines 629-632: Year followed by 'Z'
-               var gdt = GranularZonedDateTime.of("2011Z", null);
-               assertNotNull(gdt);
-               assertEquals(2011, gdt.zdt.getYear());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i43_of_yearFollowedByPlus() {
-               // Lines 633-636: Year followed by '+'
-               var gdt = GranularZonedDateTime.of("2011+05", null);
-               assertNotNull(gdt);
-               assertEquals(2011, gdt.zdt.getYear());
-               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i44_of_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.of("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_of_monthFollowedByZ() {
-               // Lines 663-666: Month followed by 'Z'
-               var gdt = GranularZonedDateTime.of("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_of_monthFollowedByPlus() {
-               // Lines 667-670: Month followed by '+'
-               var gdt = GranularZonedDateTime.of("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_of_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.of("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_of_dayFollowedByZ() {
-               // Lines 692-697: Day followed by 'Z'
-               var gdt = GranularZonedDateTime.of("2011-01-15Z", null);
-               assertNotNull(gdt);
-               assertEquals(15, gdt.zdt.getDayOfMonth());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i49_of_dayFollowedByPlus() {
-               // Lines 698-701: Day followed by '+'
-               var gdt = GranularZonedDateTime.of("2011-01-15+05", null);
-               assertNotNull(gdt);
-               assertEquals(15, gdt.zdt.getDayOfMonth());
-               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i50_of_dayFollowedByMinus() {
-               // Lines 702-705: Day followed by '-'
-               var gdt = GranularZonedDateTime.of("2011-01-15-05", null);
-               assertNotNull(gdt);
-               assertEquals(15, gdt.zdt.getDayOfMonth());
-               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i51_of_TFollowedByZ() {
-               // Lines 715-717: 'T' followed by 'Z'
-               var gdt = GranularZonedDateTime.of("TZ", null);
-               assertNotNull(gdt);
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i52_of_TFollowedByPlus() {
-               // Lines 718-720: 'T' followed by '+'
-               var gdt = GranularZonedDateTime.of("T+05", null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i53_of_TFollowedByMinus() {
-               // Lines 721-723: 'T' followed by '-'
-               var gdt = GranularZonedDateTime.of("T-05", null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i54_of_hourFollowedByZ() {
-               // Lines 732-737: Hour followed by 'Z'
-               var gdt = GranularZonedDateTime.of("2011-01-15T12Z", null);
-               assertNotNull(gdt);
-               assertEquals(12, gdt.zdt.getHour());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i55_of_hourFollowedByPlus() {
-               // Lines 738-741: Hour followed by '+'
-               var gdt = GranularZonedDateTime.of("2011-01-15T12+05", null);
-               assertNotNull(gdt);
-               assertEquals(12, gdt.zdt.getHour());
-               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i56_of_hourFollowedByMinus() {
-               // Lines 742-745: Hour followed by '-'
-               var gdt = GranularZonedDateTime.of("2011-01-15T12-05", null);
-               assertNotNull(gdt);
-               assertEquals(12, gdt.zdt.getHour());
-               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i57_of_minuteFollowedByZ() {
-               // Lines 765-768: Minute followed by 'Z'
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30Z", null);
-               assertNotNull(gdt);
-               assertEquals(30, gdt.zdt.getMinute());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i58_of_minuteFollowedByPlus() {
-               // Lines 769-772: Minute followed by '+'
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30+05", null);
-               assertNotNull(gdt);
-               assertEquals(30, gdt.zdt.getMinute());
-               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i59_of_minuteFollowedByMinus() {
-               // Lines 773-776: Minute followed by '-'
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30-05", null);
-               assertNotNull(gdt);
-               assertEquals(30, gdt.zdt.getMinute());
-               assertEquals(ZoneOffset.ofHours(-5), gdt.zdt.getOffset());
-       }
-
-       
//====================================================================================================
-       // of(String) - Timezone after fractional seconds tests
+       // of(String) and of(String, ZoneId) tests - see J_parserTests nested 
class below
        
//====================================================================================================
 
-       @Test
-       void i73_of_S13_fractionalSeparatorFollowedByZ() {
-               // Lines 817-819: S13 - '.' or ',' followed by 'Z'
-               var gdt1 = GranularZonedDateTime.of("2011-01-15T12:30:45.Z", 
null);
-               assertNotNull(gdt1);
-               assertEquals(ZoneId.of("Z"), gdt1.zdt.getZone());
-               assertEquals(0, gdt1.zdt.getNano());
-
-               var gdt2 = GranularZonedDateTime.of("2011-01-15T12:30:45,Z", 
null);
-               assertNotNull(gdt2);
-               assertEquals(ZoneId.of("Z"), gdt2.zdt.getZone());
-               assertEquals(0, gdt2.zdt.getNano());
-       }
-
-       @Test
-       void i74_of_S13_fractionalSeparatorFollowedByPlus() {
-               // Lines 820-822: S13 - '.' or ',' followed by '+'
-               var gdt1 = 
GranularZonedDateTime.of("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.of("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_of_S13_fractionalSeparatorFollowedByMinus() {
-               // Lines 823-825: S13 - '.' or ',' followed by '-'
-               var gdt1 = 
GranularZonedDateTime.of("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.of("2011-01-15T12:30:45,-05:00", null);
-               assertNotNull(gdt2);
-               assertEquals(ZoneOffset.ofHours(-5), gdt2.zdt.getOffset());
-               assertEquals(0, gdt2.zdt.getNano());
-       }
+       private static final FakeTimeProvider FAKE_TIME_PROVIDER = new 
FakeTimeProvider();
 
-       @Test
-       void i76_of_S14_fractionalSecondsFollowedByZ() {
-               // Lines 833-836: S14 - fractional seconds followed by 'Z'
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45.123Z", 
null);
-               assertNotNull(gdt);
-               assertEquals(123000000, gdt.zdt.getNano());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
+       @Nested class J_parserTests extends TestBase {
 
-       @Test
-       void i77_of_S14_fractionalSecondsFollowedByPlus() {
-               // Lines 837-840: S14 - fractional seconds followed by '+'
-               var gdt = 
GranularZonedDateTime.of("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_of_S14_fractionalSecondsFollowedByMinus() {
-               // Lines 841-844: S14 - fractional seconds followed by '-'
-               var gdt = 
GranularZonedDateTime.of("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_of_S14_fractionalSecondsWithCommaFollowedByZ() {
-               // Lines 833-836: S14 - fractional seconds (comma separator) 
followed by 'Z'
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45,123Z", 
null);
-               assertNotNull(gdt);
-               assertEquals(123000000, gdt.zdt.getNano());
-               assertEquals(ZoneId.of("Z"), gdt.zdt.getZone());
-       }
-
-       @Test
-       void i80_of_S14_fractionalSecondsWithCommaFollowedByPlus() {
-               // Lines 837-840: S14 - fractional seconds (comma separator) 
followed by '+'
-               var gdt = 
GranularZonedDateTime.of("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_of_S14_fractionalSecondsWithCommaFollowedByMinus() {
-               // Lines 841-844: S14 - fractional seconds (comma separator) 
followed by '-'
-               var gdt = 
GranularZonedDateTime.of("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_of_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.of("2011Z5", null);
-               });
-
-               // S4 -> S15 (month followed by Z, then invalid char)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01ZX", null);
-               });
-
-               // S6 -> S15 (day followed by Z, then invalid char)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15ZX", null);
-               });
-
-               // S7 -> S15 (T followed by Z, then invalid char)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("TZX", null);
-               });
-
-               // S8 -> S15 (hour followed by Z, then invalid char)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12ZX", null);
-               });
-
-               // S10 -> S15 (minute followed by Z, then invalid char)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30ZX", null);
-               });
-
-               // S12 -> S15 (second followed by Z, then invalid char)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45ZX", null);
-               });
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45Z+", null);
-               });
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45Z-", null);
-               });
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45Z5", null);
-               });
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45Z ", null);
-               });
-
-               // S13 -> S15 (fractional separator followed by Z, then invalid 
char)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45.ZX", 
null);
-               });
-
-               // S14 -> S15 (fractional digits followed by Z, then invalid 
char)
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45.123ZX", 
null);
-               });
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("T12:30:45ZX", null);
-               });
-       }
-
-       @Test
-       void i83_of_invalidDateForMonth() {
-               // Invalid dates for specific months - LocalDateTime.of() 
throws DateTimeException
-               // November only has 30 days
-               assertThrows(java.time.DateTimeException.class, () -> {
-                       GranularZonedDateTime.of("2011-11-31", null);
-               });
-               // February in non-leap year only has 28 days
-               assertThrows(java.time.DateTimeException.class, () -> {
-                       GranularZonedDateTime.of("2011-02-29", null);
-               });
-               // February in non-leap year only has 28 days (day 30)
-               assertThrows(java.time.DateTimeException.class, () -> {
-                       GranularZonedDateTime.of("2011-02-30", null);
-               });
-               // April only has 30 days
-               assertThrows(java.time.DateTimeException.class, () -> {
-                       GranularZonedDateTime.of("2011-04-31", null);
-               });
-               // June only has 30 days
-               assertThrows(java.time.DateTimeException.class, () -> {
-                       GranularZonedDateTime.of("2011-06-31", null);
-               });
-               // September only has 30 days
-               assertThrows(java.time.DateTimeException.class, () -> {
-                       GranularZonedDateTime.of("2011-09-31", null);
-               });
-               // Valid: February 29 in leap year
-               var gdt = GranularZonedDateTime.of("2024-02-29", null);
-               assertNotNull(gdt);
-               assertEquals(2024, gdt.zdt.getYear());
-               assertEquals(2, gdt.zdt.getMonthValue());
-               assertEquals(29, gdt.zdt.getDayOfMonth());
-       }
-
-       @Test
-       void i84_of_zoneIdAlreadySet() {
-               // Line 941: zoneId is not null, so offset building is skipped 
(false branch)
-               // Test with 'Z' timezone (zoneId already set)
-               var gdt = GranularZonedDateTime.of("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_of_offsetOnlyHours() {
-               // Line 946: ohour >= 0 but ominute < 0 (only hours, no 
minutes) - else if branch
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45+05", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHours(5), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i86_of_offsetHoursAndMinutes() {
-               // Line 942: ohour >= 0 && ominute >= 0 (both hours and minutes)
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45+05:30", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHoursMinutes(5, 30), 
gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i87_of_timeOnlyMissingYear() {
-               // Lines 964-966: timeOnly=true, year/month/day are -1, use 
current date
-               var now = ZonedDateTime.now();
-               var gdt = GranularZonedDateTime.of("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_of_dateFormatMissingMonth() {
-               // Line 970: timeOnly=false, month is -1, default to 1
-               var gdt = GranularZonedDateTime.of("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_of_dateFormatMissingDay() {
-               // Line 971: timeOnly=false, day is -1, default to 1
-               var gdt = GranularZonedDateTime.of("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_of_withDefaultZoneId() {
-               // Line 958: defaultZoneId != null branch - use provided 
defaultZoneId when no zone in string
-               var defaultZone = ZoneId.of("America/New_York");
-               var gdt = GranularZonedDateTime.of("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_of_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.of("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_of_withDefaultZoneId_ignoredWhenZoneInString() {
-               // Line 958: defaultZoneId is ignored when zone is found in the 
string
-               var defaultZone = ZoneId.of("America/New_York");
-               var gdt = GranularZonedDateTime.of("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());
-       }
-
-       
//====================================================================================================
-       // of(String) - ISO8601 offset range validation tests (-18:00 ≤ offset 
≤ +18:00)
-       
//====================================================================================================
-
-       @Test
-       void i60_of_offsetBoundary_minus18_00() {
-               // Minimum valid offset: -18:00
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45-18:00", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHoursMinutes(-18, 0), 
gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i61_of_offsetBoundary_plus18_00() {
-               // Maximum valid offset: +18:00
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45+18:00", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHoursMinutes(18, 0), 
gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i62_of_offsetBoundary_minus18_00_compact() {
-               // Minimum valid offset in compact format: -1800
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45-1800", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHoursMinutes(-18, 0), 
gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i63_of_offsetBoundary_plus18_00_compact() {
-               // Maximum valid offset in compact format: +1800
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45+1800", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHoursMinutes(18, 0), 
gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i64_of_offsetBoundary_minus18_00_hoursOnly() {
-               // Minimum valid offset hours only: -18
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45-18", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHours(-18), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i65_of_offsetBoundary_plus18_00_hoursOnly() {
-               // Maximum valid offset hours only: +18
-               var gdt = GranularZonedDateTime.of("2011-01-15T12:30:45+18", 
null);
-               assertNotNull(gdt);
-               assertEquals(ZoneOffset.ofHours(18), gdt.zdt.getOffset());
-       }
-
-       @Test
-       void i66_of_offsetInvalid_belowMinimum() {
-               // Invalid: offset below -18:00
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45-19:00", 
null);
-               });
-       }
-
-       @Test
-       void i67_of_offsetInvalid_aboveMaximum() {
-               // Invalid: offset above +18:00
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45+19:00", 
null);
-               });
-       }
-
-       @Test
-       void i68_of_offsetInvalid_belowMinimum_compact() {
-               // Invalid: offset below -18:00 in compact format
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45-1900", 
null);
-               });
-       }
+               record ParseTest(int index, String name, String input, String 
expected, ZoneId defaultZoneId) {
+                       ParseTest(int index, String name, String input, String 
expected) {
+                               this(index, name, input, expected, null);
+                       }
+               }
 
-       @Test
-       void i69_of_offsetInvalid_aboveMaximum_compact() {
-               // Invalid: offset above +18:00 in compact format
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45+1900", 
null);
-               });
-       }
+               static ParseTest[] parseTests() {
+                       return new ParseTest[] {
+                               // Date formats
+                               new ParseTest(1, "yearOnly", "2011", 
"2011-01-01T00:00:00Z(Year)"),
+                               new ParseTest(2, "yearMonth", "2011-01", 
"2011-01-01T00:00:00Z(MonthOfYear)"),
+                               new ParseTest(3, "date", "2011-01-15", 
"2011-01-15T00:00:00Z(DayOfMonth)"),
+
+                               // DateTime formats
+                               new ParseTest(4, "dateTime_hour", 
"2011-01-15T12", "2011-01-15T12:00:00Z(HourOfDay)"),
+                               new ParseTest(5, "dateTime_minute", 
"2011-01-15T12:30", "2011-01-15T12:30:00Z(MinuteOfHour)"),
+                               new ParseTest(6, "dateTime_second", 
"2011-01-15T12:30:45", "2011-01-15T12:30:45Z(SecondOfMinute)"),
+
+                               // With fractional seconds
+                               new ParseTest(7, "dateTime_millisecond_dot", 
"2011-01-15T12:30:45.123", "2011-01-15T12:30:45.123Z(MilliOfSecond)"),
+                               new ParseTest(8, "dateTime_millisecond_comma", 
"2011-01-15T12:30:45,123", "2011-01-15T12:30:45.123Z(MilliOfSecond)"),
+                               new ParseTest(9, "dateTime_nanosecond_1digit", 
"2011-01-15T12:30:45.1", "2011-01-15T12:30:45.1Z(MilliOfSecond)"),
+                               new ParseTest(10, 
"dateTime_nanosecond_2digits", "2011-01-15T12:30:45.12", 
"2011-01-15T12:30:45.12Z(MilliOfSecond)"),
+                               new ParseTest(11, 
"dateTime_nanosecond_3digits", "2011-01-15T12:30:45.123", 
"2011-01-15T12:30:45.123Z(MilliOfSecond)"),
+                               new ParseTest(12, 
"dateTime_nanosecond_4digits", "2011-01-15T12:30:45.1234", 
"2011-01-15T12:30:45.1234Z(NanoOfSecond)"),
+                               new ParseTest(13, 
"dateTime_nanosecond_5digits", "2011-01-15T12:30:45.12345", 
"2011-01-15T12:30:45.12345Z(NanoOfSecond)"),
+                               new ParseTest(14, 
"dateTime_nanosecond_6digits", "2011-01-15T12:30:45.123456", 
"2011-01-15T12:30:45.123456Z(NanoOfSecond)"),
+                               new ParseTest(15, 
"dateTime_nanosecond_7digits", "2011-01-15T12:30:45.1234567", 
"2011-01-15T12:30:45.1234567Z(NanoOfSecond)"),
+                               new ParseTest(16, 
"dateTime_nanosecond_8digits", "2011-01-15T12:30:45.12345678", 
"2011-01-15T12:30:45.12345678Z(NanoOfSecond)"),
+                               new ParseTest(17, 
"dateTime_nanosecond_9digits", "2011-01-15T12:30:45.123456789", 
"2011-01-15T12:30:45.123456789Z(NanoOfSecond)"),
+
+                               // Time-only formats (use fixed current time: 
2000-01-01T12:00:00Z)
+                               new ParseTest(18, "timeOnly_hour", "T12", 
"2000-01-01T12:00:00Z(HourOfDay)"),
+                               new ParseTest(19, "timeOnly_minute", "T12:30", 
"2000-01-01T12:30:00Z(MinuteOfHour)"),
+                               new ParseTest(20, "timeOnly_second", 
"T12:30:45", "2000-01-01T12:30:45Z(SecondOfMinute)"),
+                               new ParseTest(21, "timeOnly_millisecond", 
"T12:30:45.123", "2000-01-01T12:30:45.123Z(MilliOfSecond)"),
+                               new ParseTest(22, "timeOnly_withZ", 
"T12:30:45Z", "2000-01-01T12:30:45Z(SecondOfMinute)"),
+                               new ParseTest(23, "timeOnly_withOffset", 
"T12:30:45+05:30", "2000-01-01T12:30:45+05:30(SecondOfMinute)"),
+
+                               // With UTC timezone
+                               new ParseTest(24, "withZ", 
"2011-01-15T12:30:45Z", "2011-01-15T12:30:45Z(SecondOfMinute)"),
+                               new ParseTest(25, "yearWithZ", "2011Z", 
"2011-01-01T00:00:00Z(Year)"),
+                               new ParseTest(26, "yearMonthWithZ", "2011-01Z", 
"2011-01-01T00:00:00Z(MonthOfYear)"),
+                               new ParseTest(27, "dateWithZ", "2011-01-15Z", 
"2011-01-15T00:00:00Z(DayOfMonth)"),
+                               new ParseTest(28, "hourWithZ", 
"2011-01-15T12Z", "2011-01-15T12:00:00Z(HourOfDay)"),
+                               new ParseTest(29, "minuteWithZ", 
"2011-01-15T12:30Z", "2011-01-15T12:30:00Z(MinuteOfHour)"),
+                               new ParseTest(30, "millisecondWithZ", 
"2011-01-15T12:30:45.123Z", "2011-01-15T12:30:45.123Z(MilliOfSecond)"),
+
+                               // With offset (hours only)
+                               new ParseTest(31, "offset_plusHH", 
"2011-01-15T12:30:45+05", "2011-01-15T12:30:45+05:00(SecondOfMinute)"),
+                               new ParseTest(32, "offset_minusHH", 
"2011-01-15T12:30:45-05", "2011-01-15T12:30:45-05:00(SecondOfMinute)"),
+
+                               // With offset (hours and minutes, compact)
+                               new ParseTest(33, "offset_plusHHMM", 
"2011-01-15T12:30:45+0530", "2011-01-15T12:30:45+05:30(SecondOfMinute)"),
+                               new ParseTest(34, "offset_minusHHMM", 
"2011-01-15T12:30:45-0530", "2011-01-15T12:30:45-05:30(SecondOfMinute)"),
+
+                               // With offset (hours and minutes, with colon)
+                               new ParseTest(35, "offset_plusHH_MM", 
"2011-01-15T12:30:45+05:30", "2011-01-15T12:30:45+05:30(SecondOfMinute)"),
+                               new ParseTest(36, "offset_minusHH_MM", 
"2011-01-15T12:30:45-05:30", "2011-01-15T12:30:45-05:30(SecondOfMinute)"),
+                               new ParseTest(37, "offset_minus12_30", 
"2011-01-15T12:30:45-12:30", "2011-01-15T12:30:45-12:30(SecondOfMinute)"),
+                               new ParseTest(38, "offset_plus09_00", 
"2011-01-15T12:30:45+09:00", "2011-01-15T12:30:45+09:00(SecondOfMinute)"),
+
+                               // Timezone after various components
+                               new ParseTest(39, "yearFollowedByT", "2011T12", 
"2011-01-01T12:00:00Z(HourOfDay)"),
+                               new ParseTest(40, "yearFollowedByPlus", 
"2011+05", "2011-01-01T00:00:00+05:00(Year)"),
+                               new ParseTest(41, "yearFollowedByMinus", 
"2011-01-15T12-05", "2011-01-15T12:00:00-05:00(HourOfDay)"),
+                               new ParseTest(42, "monthFollowedByZ", 
"2011-01Z", "2011-01-01T00:00:00Z(MonthOfYear)"),
+                               new ParseTest(43, "monthFollowedByPlus", 
"2011-01+05", "2011-01-01T00:00:00+05:00(MonthOfYear)"),
+                               new ParseTest(44, "monthFollowedByMinus", 
"2011-01-15T12:30-05", "2011-01-15T12:30:00-05:00(MinuteOfHour)"),
+                               new ParseTest(45, "dayFollowedByZ", 
"2011-01-15Z", "2011-01-15T00:00:00Z(DayOfMonth)"),
+                               new ParseTest(46, "dayFollowedByPlus", 
"2011-01-15+05", "2011-01-15T00:00:00+05:00(DayOfMonth)"),
+                               new ParseTest(47, "dayFollowedByMinus", 
"2011-01-15-05", "2011-01-15T00:00:00-05:00(DayOfMonth)"),
+                               new ParseTest(48, "hourFollowedByZ", 
"2011-01-15T12Z", "2011-01-15T12:00:00Z(HourOfDay)"),
+                               new ParseTest(49, "hourFollowedByPlus", 
"2011-01-15T12+05", "2011-01-15T12:00:00+05:00(HourOfDay)"),
+                               new ParseTest(50, "hourFollowedByMinus", 
"2011-01-15T12-05", "2011-01-15T12:00:00-05:00(HourOfDay)"),
+                               new ParseTest(51, "minuteFollowedByZ", 
"2011-01-15T12:30Z", "2011-01-15T12:30:00Z(MinuteOfHour)"),
+                               new ParseTest(52, "minuteFollowedByPlus", 
"2011-01-15T12:30+05", "2011-01-15T12:30:00+05:00(MinuteOfHour)"),
+                               new ParseTest(53, "minuteFollowedByMinus", 
"2011-01-15T12:30-05", "2011-01-15T12:30:00-05:00(MinuteOfHour)"),
+                               new ParseTest(54, "timezoneAfterT", 
"2011-01T+05:30", "2011-01-01T00:00:00+05:30(MonthOfYear)"),
+                               new ParseTest(55, "dateFollowedByTZ", 
"2011-01TZ", "2011-01-01T00:00:00Z(MonthOfYear)"),
+                               new ParseTest(56, "dateFollowedByTMinus", 
"2011-01T-05:30", "2011-01-01T00:00:00-05:30(MonthOfYear)"),
+                               new ParseTest(57, "TFollowedByZ", "TZ", 
"2000-01-01T00:00:00Z(HourOfDay)"),
+                               new ParseTest(58, "TFollowedByPlus", "T+05", 
"2000-01-01T00:00:00+05:00(HourOfDay)"),
+                               new ParseTest(59, "TFollowedByMinus", "T-05", 
"2000-01-01T00:00:00-05:00(HourOfDay)"),
+
+                               // Fractional separator followed by timezone
+                               new ParseTest(60, 
"fractionalSeparatorDotFollowedByZ", "2011-01-15T12:30:45.Z", 
"2011-01-15T12:30:45Z(SecondOfMinute)"),
+                               new ParseTest(61, 
"fractionalSeparatorCommaFollowedByZ", "2011-01-15T12:30:45,Z", 
"2011-01-15T12:30:45Z(SecondOfMinute)"),
+                               new ParseTest(62, 
"fractionalSeparatorDotFollowedByPlus", "2011-01-15T12:30:45.+05:00", 
"2011-01-15T12:30:45+05:00(SecondOfMinute)"),
+                               new ParseTest(63, 
"fractionalSeparatorCommaFollowedByPlus", "2011-01-15T12:30:45,+05:00", 
"2011-01-15T12:30:45+05:00(SecondOfMinute)"),
+                               new ParseTest(64, 
"fractionalSeparatorDotFollowedByMinus", "2011-01-15T12:30:45.-05:00", 
"2011-01-15T12:30:45-05:00(SecondOfMinute)"),
+                               new ParseTest(65, 
"fractionalSeparatorCommaFollowedByMinus", "2011-01-15T12:30:45,-05:00", 
"2011-01-15T12:30:45-05:00(SecondOfMinute)"),
+
+                               // Fractional seconds followed by timezone
+                               new ParseTest(66, 
"fractionalSecondsFollowedByZ", "2011-01-15T12:30:45.123Z", 
"2011-01-15T12:30:45.123Z(MilliOfSecond)"),
+                               new ParseTest(67, 
"fractionalSecondsFollowedByPlus", "2011-01-15T12:30:45.123+05:00", 
"2011-01-15T12:30:45.123+05:00(MilliOfSecond)"),
+                               new ParseTest(68, 
"fractionalSecondsFollowedByMinus", "2011-01-15T12:30:45.123-05:00", 
"2011-01-15T12:30:45.123-05:00(MilliOfSecond)"),
+                               new ParseTest(69, 
"fractionalSecondsCommaFollowedByZ", "2011-01-15T12:30:45,123Z", 
"2011-01-15T12:30:45.123Z(MilliOfSecond)"),
+                               new ParseTest(70, 
"fractionalSecondsCommaFollowedByPlus", "2011-01-15T12:30:45,123+05:00", 
"2011-01-15T12:30:45.123+05:00(MilliOfSecond)"),
+                               new ParseTest(71, 
"fractionalSecondsCommaFollowedByMinus", "2011-01-15T12:30:45,123-05:00", 
"2011-01-15T12:30:45.123-05:00(MilliOfSecond)"),
+
+                               // Nanoseconds (4+ digits) followed by timezone
+                               new ParseTest(72, "nanosecondsFollowedByZ", 
"2011-01-15T12:30:45.1234Z", "2011-01-15T12:30:45.1234Z(NanoOfSecond)"),
+                               new ParseTest(73, "nanosecondsFollowedByPlus", 
"2011-01-15T12:30:45.1234+05:00", 
"2011-01-15T12:30:45.1234+05:00(NanoOfSecond)"),
+                               new ParseTest(74, "nanosecondsFollowedByMinus", 
"2011-01-15T12:30:45.1234-05:00", 
"2011-01-15T12:30:45.1234-05:00(NanoOfSecond)"),
+
+                               // ISO8601 offset range validation (-18:00 ≤ 
offset ≤ +18:00)
+                               new ParseTest(75, "offsetBoundary_minus18_00", 
"2011-01-15T12:30:45-18:00", "2011-01-15T12:30:45-18:00(SecondOfMinute)"),
+                               new ParseTest(76, "offsetBoundary_plus18_00", 
"2011-01-15T12:30:45+18:00", "2011-01-15T12:30:45+18:00(SecondOfMinute)"),
+                               new ParseTest(77, 
"offsetBoundary_minus18_00_compact", "2011-01-15T12:30:45-1800", 
"2011-01-15T12:30:45-18:00(SecondOfMinute)"),
+                               new ParseTest(78, 
"offsetBoundary_plus18_00_compact", "2011-01-15T12:30:45+1800", 
"2011-01-15T12:30:45+18:00(SecondOfMinute)"),
+                               new ParseTest(79, 
"offsetBoundary_minus18_hoursOnly", "2011-01-15T12:30:45-18", 
"2011-01-15T12:30:45-18:00(SecondOfMinute)"),
+                               new ParseTest(80, 
"offsetBoundary_plus18_hoursOnly", "2011-01-15T12:30:45+18", 
"2011-01-15T12:30:45+18:00(SecondOfMinute)"),
+                               new ParseTest(81, "offsetValid_withinRange1", 
"2011-01-15T12:30:45-12:30", "2011-01-15T12:30:45-12:30(SecondOfMinute)"),
+                               new ParseTest(82, "offsetValid_withinRange2", 
"2011-01-15T12:30:45+12:30", "2011-01-15T12:30:45+12:30(SecondOfMinute)"),
+                               new ParseTest(83, "offsetValid_withinRange3", 
"2011-01-15T12:30:45-17:59", "2011-01-15T12:30:45-17:59(SecondOfMinute)"),
+                               new ParseTest(84, "offsetValid_withinRange4", 
"2011-01-15T12:30:45+17:59", "2011-01-15T12:30:45+17:59(SecondOfMinute)"),
+
+                               // Invalid offset range
+                               new ParseTest(85, "offsetInvalid_belowMinimum", 
"2011-01-15T12:30:45-19:00", "Invalid ISO8601 timestamp"),
+                               new ParseTest(86, "offsetInvalid_aboveMaximum", 
"2011-01-15T12:30:45+19:00", "Invalid ISO8601 timestamp"),
+                               new ParseTest(87, 
"offsetInvalid_belowMinimum_compact", "2011-01-15T12:30:45-1900", "Invalid 
ISO8601 timestamp"),
+                               new ParseTest(88, 
"offsetInvalid_aboveMaximum_compact", "2011-01-15T12:30:45+1900", "Invalid 
ISO8601 timestamp"),
+                               new ParseTest(89, 
"offsetInvalid_belowMinimum_hoursOnly", "2011-01-15T12:30:45-19", "Invalid 
ISO8601 timestamp"),
+                               new ParseTest(90, 
"offsetInvalid_aboveMaximum_hoursOnly", "2011-01-15T12:30:45+19", "Invalid 
ISO8601 timestamp"),
+
+                               // Invalid offset format
+                               new ParseTest(91, "offsetInvalid_1digit", 
"2011-01-15T12:30:45+1", "Invalid ISO8601 timestamp"),
+                               new ParseTest(92, "offsetInvalid_3digits", 
"2011-01-15T12:30:45+123", "Invalid ISO8601 timestamp"),
+                               new ParseTest(93, "offsetInvalid_5digits", 
"2011-01-15T12:30:45+12345", "Invalid ISO8601 timestamp"),
+
+                               // Invalid date/time values
+                               new ParseTest(94, "invalidYearLength", "123", 
"Invalid ISO8601 timestamp"),
+                               new ParseTest(95, "invalidMonth_00", "2011-00", 
"Invalid ISO8601 timestamp"),
+                               new ParseTest(96, "invalidMonth_13", "2011-13", 
"Invalid ISO8601 timestamp"),
+                               new ParseTest(97, "invalidMonth_99", "2011-99", 
"Invalid ISO8601 timestamp"),
+                               new ParseTest(98, "invalidDay_00", 
"2011-01-00", "Invalid ISO8601 timestamp"),
+                               new ParseTest(99, "invalidDay_32", 
"2011-01-32", "Invalid ISO8601 timestamp"),
+                               new ParseTest(100, "invalidDay_99", 
"2011-01-99", "Invalid ISO8601 timestamp"),
+                               new ParseTest(101, "invalidHour_24", 
"2011-01-15T24", "Invalid ISO8601 timestamp"),
+                               new ParseTest(102, "invalidHour_99", 
"2011-01-15T99", "Invalid ISO8601 timestamp"),
+                               new ParseTest(103, "invalidMinute_60", 
"2011-01-15T12:60", "Invalid ISO8601 timestamp"),
+                               new ParseTest(104, "invalidMinute_99", 
"2011-01-15T12:99", "Invalid ISO8601 timestamp"),
+                               new ParseTest(105, "invalidSecond_60", 
"2011-01-15T12:30:60", "Invalid ISO8601 timestamp"),
+                               new ParseTest(106, "invalidSecond_99", 
"2011-01-15T12:30:99", "Invalid ISO8601 timestamp"),
+
+                               // Invalid dates for specific months
+                               new ParseTest(107, "invalidDate_Nov31", 
"2011-11-31", "Invalid date 'NOVEMBER 31'"),
+                               new ParseTest(108, "invalidDate_Feb29_nonLeap", 
"2011-02-29", "Invalid date 'February 29' as '2011' is not a leap year"),
+                               new ParseTest(109, "invalidDate_Feb30", 
"2011-02-30", "Invalid date 'FEBRUARY 30'"),
+                               new ParseTest(110, "invalidDate_Apr31", 
"2011-04-31", "Invalid date 'APRIL 31'"),
+                               new ParseTest(111, "invalidDate_Jun31", 
"2011-06-31", "Invalid date 'JUNE 31'"),
+                               new ParseTest(112, "invalidDate_Sep31", 
"2011-09-31", "Invalid date 'SEPTEMBER 31'"),
+                               new ParseTest(113, "validDate_Feb29_leap", 
"2024-02-29", "2024-02-29T00:00:00Z(DayOfMonth)"),
+
+                               // Invalid characters in various states
+                               new ParseTest(114, "invalidCharAfterYear", 
"2011X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(115, "invalidCharAfterYearDash", 
"2011-X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(116, "invalidCharAfterMonth", 
"2011-01X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(117, "invalidCharAfterMonthDash", 
"2011-01-X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(118, "invalidCharAfterDay", 
"2011-01-15X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(119, "invalidCharAfterT", "TX", 
"Invalid ISO8601 timestamp"),
+                               new ParseTest(120, "invalidCharAfterHour", 
"2011-01-15T12X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(121, "invalidCharAfterHourColon", 
"2011-01-15T12:X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(122, "invalidCharAfterMinute", 
"2011-01-15T12:30X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(123, 
"invalidCharAfterMinuteColon", "2011-01-15T12:30:X", "Invalid ISO8601 
timestamp"),
+                               new ParseTest(124, "invalidCharAfterSecond", 
"2011-01-15T12:30:45X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(125, 
"invalidCharAfterFractionalSeparator", "2011-01-15T12:30:45.X", "Invalid 
ISO8601 timestamp"),
+                               new ParseTest(126, 
"invalidCharInFractionalSeconds", "2011-01-15T12:30:45.12X", "Invalid ISO8601 
timestamp"),
+                               new ParseTest(127, "invalidCharAfterPlus", 
"2011-01-15T12:30:45+X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(128, "invalidCharAfterMinus", 
"2011-01-15T12:30:45-X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(129, "invalidCharInOffsetHours", 
"2011-01-15T12:30:45+05X", "Invalid ISO8601 timestamp"),
+                               new ParseTest(130, 
"invalidCharAfterOffsetColon", "2011-01-15T12:30:45+05:X", "Invalid ISO8601 
timestamp"),
+                               new ParseTest(131, 
"invalidCharInOffsetMinutes", "2011-01-15T12:30:45+05:30X", "Invalid ISO8601 
timestamp"),
+
+                               // Invalid character after Z
+                               new ParseTest(132, "invalidCharAfterZ_year", 
"2011Z5", "Invalid ISO8601 timestamp"),
+                               new ParseTest(133, "invalidCharAfterZ_month", 
"2011-01ZX", "Invalid ISO8601 timestamp"),
+                               new ParseTest(134, "invalidCharAfterZ_day", 
"2011-01-15ZX", "Invalid ISO8601 timestamp"),
+                               new ParseTest(135, "invalidCharAfterZ_T", 
"TZX", "Invalid ISO8601 timestamp"),
+                               new ParseTest(136, "invalidCharAfterZ_hour", 
"2011-01-15T12ZX", "Invalid ISO8601 timestamp"),
+                               new ParseTest(137, "invalidCharAfterZ_minute", 
"2011-01-15T12:30ZX", "Invalid ISO8601 timestamp"),
+                               new ParseTest(138, "invalidCharAfterZ_second", 
"2011-01-15T12:30:45ZX", "Invalid ISO8601 timestamp"),
+                               new ParseTest(139, 
"invalidCharAfterZ_secondPlus", "2011-01-15T12:30:45Z+", "Invalid ISO8601 
timestamp"),
+                               new ParseTest(140, 
"invalidCharAfterZ_secondMinus", "2011-01-15T12:30:45Z-", "Invalid ISO8601 
timestamp"),
+                               new ParseTest(141, 
"invalidCharAfterZ_secondDigit", "2011-01-15T12:30:45Z5", "Invalid ISO8601 
timestamp"),
+                               new ParseTest(142, 
"invalidCharAfterZ_secondSpace", "2011-01-15T12:30:45Z ", "Invalid ISO8601 
timestamp"),
+                               new ParseTest(143, 
"invalidCharAfterZ_fractionalSeparator", "2011-01-15T12:30:45.ZX", "Invalid 
ISO8601 timestamp"),
+                               new ParseTest(144, 
"invalidCharAfterZ_fractionalSeconds", "2011-01-15T12:30:45.123ZX", "Invalid 
ISO8601 timestamp"),
+                               new ParseTest(145, 
"invalidCharAfterZ_timeOnly", "T12:30:45ZX", "Invalid ISO8601 timestamp"),
+
+                               // Invalid nanosecond length (>9 digits)
+                               new ParseTest(146, "invalidNanos_10digits", 
"2011-01-15T12:30:45.1234567890", "Invalid ISO8601 timestamp"),
+
+                               // Edge cases with defaultZoneId
+                               new ParseTest(147, "withDefaultZoneId", 
"2011-01-15T12:30:45", "2011-01-15T12:30:45-05:00(SecondOfMinute)", 
ZoneId.of("America/New_York")),
+                               new ParseTest(148, 
"withDefaultZoneId_timeOnly", "T12:30:45", 
"2000-01-01T12:30:45-05:00(SecondOfMinute)", ZoneId.of("America/New_York")),
+                               new ParseTest(149, 
"withDefaultZoneId_ignoredWhenZoneInString", "2011-01-15T12:30:45Z", 
"2011-01-15T12:30:45Z(SecondOfMinute)", ZoneId.of("America/New_York")),
+
+                               // Invalid inputs (expect exceptions)
+                               new ParseTest(150, "null", null, "Argument 
'value' cannot be null."),
+                               new ParseTest(151, "emptyString", "", "Invalid 
ISO8601 timestamp"),
+                               new ParseTest(152, "invalidString", 
"invalid-date", "Invalid ISO8601 timestamp"),
+                       };
+               }
 
-       @Test
-       void i70_of_offsetInvalid_belowMinimum_hoursOnly() {
-               // Invalid: offset below -18 hours only
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45-19", 
null);
-               });
-       }
+               @ParameterizedTest(name = "[{0}] {1}")
+               @MethodSource("parseTests")
+               void j01_parse(ParseTest test) {
+                       if (test.defaultZoneId == null) {
+                               testParse(test.expected, test.input);
+                       } else {
+                               testParse(test.expected, test.input, 
test.defaultZoneId);
+                       }
+               }
 
-       @Test
-       void i71_of_offsetInvalid_aboveMaximum_hoursOnly() {
-               // Invalid: offset above +18 hours only
-               assertThrows(java.time.format.DateTimeParseException.class, () 
-> {
-                       GranularZonedDateTime.of("2011-01-15T12:30:45+19", 
null);
-               });
-       }
+               private void testParse(String expected, String in) {
+                       try {
+                               var x = GranularZonedDateTime.of(in, 
FAKE_TIME_PROVIDER);
+                               assertEquals(expected, x.toString(), "Failed 
for input: " + in);
+                       } catch (Exception e) {
+                               assertEquals(expected, e.getLocalizedMessage(), 
"Failed for input: " + in);
+                       }
+               }
 
-       @Test
-       void i72_of_offsetValid_withinRange() {
-               // Valid offsets within range
-               var gdt1 = 
GranularZonedDateTime.of("2011-01-15T12:30:45-12:30", null);
-               assertNotNull(gdt1);
-               assertEquals(ZoneOffset.ofHoursMinutes(-12, -30), 
gdt1.zdt.getOffset());
-
-               var gdt2 = 
GranularZonedDateTime.of("2011-01-15T12:30:45+12:30", null);
-               assertNotNull(gdt2);
-               assertEquals(ZoneOffset.ofHoursMinutes(12, 30), 
gdt2.zdt.getOffset());
-
-               var gdt3 = 
GranularZonedDateTime.of("2011-01-15T12:30:45-17:59", null);
-               assertNotNull(gdt3);
-               assertEquals(ZoneOffset.ofHoursMinutes(-17, -59), 
gdt3.zdt.getOffset());
-
-               var gdt4 = 
GranularZonedDateTime.of("2011-01-15T12:30:45+17:59", null);
-               assertNotNull(gdt4);
-               assertEquals(ZoneOffset.ofHoursMinutes(17, 59), 
gdt4.zdt.getOffset());
+               private void testParse(String expected, String in, ZoneId 
zoneId) {
+                       try {
+                               var x = GranularZonedDateTime.of(in, zoneId, 
FAKE_TIME_PROVIDER);
+                               assertEquals(expected, x.toString(), "Failed 
for input: " + in + " with zoneId: " + zoneId);
+                       } catch (Exception e) {
+                               assertEquals(expected, e.getLocalizedMessage(), 
"Failed for input: " + in + " with zoneId: " + zoneId);
+                       }
+               }
        }
 }
-
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/oapi/OpenApi_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/oapi/OpenApi_Test.java
index 854024bafe..dcc3c288ec 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/oapi/OpenApi_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/oapi/OpenApi_Test.java
@@ -31,6 +31,7 @@ import org.apache.juneau.commons.time.*;
 import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.serializer.*;
+import org.apache.juneau.utest.utils.FakeTimeProvider;
 import org.junit.jupiter.api.*;
 
 /**
@@ -38,6 +39,8 @@ import org.junit.jupiter.api.*;
  */
 public class OpenApi_Test extends TestBase {
 
+       private static final FakeTimeProvider FAKE_TIME_PROVIDER = new 
FakeTimeProvider();
+
        public static final OpenApiSerializer DS = OpenApiSerializer.DEFAULT;
        public static final OpenApiParser DP = OpenApiParser.DEFAULT;
 
@@ -862,6 +865,6 @@ public class OpenApi_Test extends TestBase {
        
//---------------------------------------------------------------------------------------------
 
        private static Calendar cal(String in) {
-               return opt(in).filter(x1 -> ! isBlank(x1)).map(x -> 
GranularZonedDateTime.of(in).getZonedDateTime()).map(GregorianCalendar::from).orElse(null);
+               return opt(in).filter(x1 -> ! isBlank(x1)).map(x -> 
GranularZonedDateTime.of(in, 
FAKE_TIME_PROVIDER).getZonedDateTime()).map(GregorianCalendar::from).orElse(null);
        }
 }
\ No newline at end of file
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/utest/utils/FakeTimeProvider.java
 
b/juneau-utest/src/test/java/org/apache/juneau/utest/utils/FakeTimeProvider.java
new file mode 100644
index 0000000000..a8f5f99a6c
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/utest/utils/FakeTimeProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.utest.utils;
+
+import java.time.*;
+
+import org.apache.juneau.commons.time.TimeProvider;
+
+/**
+ * A fake time provider for testing that always returns UTC timezone and a 
fixed time.
+ */
+public class FakeTimeProvider extends TimeProvider {
+
+       private static final ZoneId UTC = ZoneId.of("UTC");
+       private static final ZonedDateTime FIXED_TIME = ZonedDateTime.of(2000, 
1, 1, 12, 0, 0, 0, UTC);
+
+       @Override
+       public ZoneId getSystemDefaultZoneId() {
+               return UTC;
+       }
+
+       @Override
+       public ZonedDateTime now() {
+               return FIXED_TIME;
+       }
+
+       @Override
+       public ZonedDateTime now(ZoneId zoneId) {
+               return FIXED_TIME.withZoneSameInstant(zoneId);
+       }
+}
+

Reply via email to