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