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

commit 4cfe1de3259cdaf914db48b48d2101959fb99795
Author: James Bognar <[email protected]>
AuthorDate: Wed Dec 3 18:07:06 2025 -0800

    Unit tests
---
 .../org/apache/juneau/commons/utils/DateUtils.java | 535 ++------------------
 .../commons/utils/GranularZonedDateTime.java       | 230 ++++++++-
 .../apache/juneau/commons/utils/StringUtils.java   |  50 --
 .../apache/juneau/oapi/OpenApiParserSession.java   |   2 +-
 .../microservice/resources/LogsResource.java       |   9 +-
 .../juneau/rest/mock/MockServletResponse.java      |  14 +-
 .../juneau/commons/utils/DateUtils_Test.java       | 557 +++------------------
 .../juneau/commons/utils/StringUtils_Test.java     |  78 ---
 .../httppart/OpenApiPartSerializer_Test.java       |  10 +-
 9 files changed, 379 insertions(+), 1106 deletions(-)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/DateUtils.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/DateUtils.java
index 32e9c7912e..07e567ec1f 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/DateUtils.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/DateUtils.java
@@ -41,125 +41,6 @@ import java.util.*;
  */
 public class DateUtils {
 
-       /**
-        * A factory for {@link SimpleDateFormat}s.
-        *
-        * <p>
-        * The instances are stored in a thread-local way because 
SimpleDateFormat is not thread-safe as noted in
-        * {@link SimpleDateFormat its javadoc}.
-        */
-       static class DateFormatHolder {
-               private static final 
ThreadLocal<SoftReference<Map<String,SimpleDateFormat>>> THREADLOCAL_FORMATS = 
new ThreadLocal<>() {
-                       @Override
-                       protected SoftReference<Map<String,SimpleDateFormat>> 
initialValue() {
-                               var m = new HashMap<String,SimpleDateFormat>();
-                               return new SoftReference<>(m);
-                       }
-               };
-
-               public static void clearThreadLocal() {
-                       THREADLOCAL_FORMATS.remove();
-               }
-
-               /**
-                * Creates a {@link SimpleDateFormat} for the requested format 
string.
-                *
-                * @param pattern
-                *      A non-<c>null</c> format String according to {@link 
SimpleDateFormat}.
-                *      The format is not checked against <c>null</c> since all 
paths go through {@link DateUtils}.
-                * @return
-                *      The requested format.
-                *      This simple date-format should not be used to {@link 
SimpleDateFormat#applyPattern(String) apply} to a
-                *      different pattern.
-                */
-               public static SimpleDateFormat formatFor(String pattern) {
-                       var ref = THREADLOCAL_FORMATS.get();
-                       var formats = ref.get();
-                       if (formats == null) {
-                               formats = new HashMap<>();
-                               THREADLOCAL_FORMATS.set(new 
SoftReference<>(formats));
-                       }
-                       var format = formats.get(pattern);
-                       if (format == null) {
-                               format = new SimpleDateFormat(pattern, 
Locale.US);
-                               format.setTimeZone(TimeZone.getTimeZone("GMT"));
-                               formats.put(pattern, format);
-                       }
-                       return format;
-               }
-       }
-
-       /**
-        * Date format pattern used to parse HTTP date headers in RFC 1123 
format.
-        */
-       public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss 
zzz";
-
-       /**
-        * Date format pattern used to parse HTTP date headers in RFC 1036 
format.
-        */
-       public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss 
zzz";
-       /**
-        * Date format pattern used to parse HTTP date headers in ANSI C 
<c>asctime()</c> format.
-        */
-       public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
-       private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
-
-       static {
-               var calendar = Calendar.getInstance();
-               calendar.setTimeZone(GMT);
-               calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
-               calendar.set(Calendar.MILLISECOND, 0);
-       }
-
-       /**
-        * Adds to a field of a calendar.
-        *
-        * @param c The calendar to modify.
-        * @param field The calendar field to modify (e.g., {@link 
Calendar#DATE}, {@link Calendar#MONTH}).
-        * @param amount The amount to add.
-        * @return The same calendar with the field modified.
-        */
-       public static Calendar add(Calendar c, int field, int amount) {
-               c.add(field, amount);
-               return c;
-       }
-
-       /**
-        * Adds or subtracts a number of days from the specified calendar.
-        *
-        * <p>Creates a clone of the calendar before modifying it.
-        *
-        * @param c The calendar to modify.
-        * @param days The number of days to add (positive) or subtract 
(negative).
-        * @return A cloned calendar with the updated date, or <jk>null</jk> if 
the input was <jk>null</jk>.
-        */
-       public static Calendar addSubtractDays(Calendar c, int days) {
-               return opt(c).map(x -> (Calendar)x.clone()).map(x -> add(x, 
Calendar.DATE, days)).orElse(null);
-       }
-
-       /**
-        * Clears thread-local variable containing {@link java.text.DateFormat} 
cache.
-        */
-       public static void clearThreadLocal() {
-               DateFormatHolder.clearThreadLocal();
-       }
-
-       /**
-        * Formats the given date according to the specified pattern.
-        *
-        * <p>
-        * The pattern must conform to that used by the {@link SimpleDateFormat 
simple date format} class.
-        *
-        * @param date The date to format.
-        * @param pattern The pattern to use for formatting the date.
-        * @return A formatted date string.
-        * @throws IllegalArgumentException If the given date pattern is 
invalid.
-        * @see SimpleDateFormat
-        */
-       public static String formatDate(Date date, String pattern) {
-               return DateFormatHolder.formatFor(pattern).format(date);
-       }
-
        /**
         * Parses an ISO8601 date string into a ZonedDateTime object.
         *
@@ -290,7 +171,6 @@ public class DateUtils {
         * @return Calendar object representing the parsed date/time, or null 
if input is null/empty
         * @throws DateTimeParseException if the string cannot be parsed as a 
valid ISO8601 date
         * @see #fromIso8601(String)
-        * @see #toIso8601(Calendar)
         */
        public static Calendar fromIso8601Calendar(String s) {
                if (isBlank(s))
@@ -319,375 +199,11 @@ public class DateUtils {
                }
        }
 
-       /**
-        * Determines the precision level of an ISO8601 date/time string using 
a state machine.
-        *
-        * <p>
-        * This method analyzes the structure of a date/time string to 
determine the finest level of precision
-        * represented. It uses a state machine to parse the string character 
by character, tracking the precision
-        * level as it encounters different components.
-        *
-        * <p>
-        * The method supports the following ISO8601 formats:
-        * <ul>
-        *      <li><js>"YYYY"</js> → {@link ChronoField#YEAR}
-        *      <li><js>"YYYY-MM"</js> → {@link ChronoField#MONTH_OF_YEAR}
-        *      <li><js>"YYYY-MM-DD"</js> → {@link ChronoField#DAY_OF_MONTH}
-        *      <li><js>"YYYY-MM-DDTHH"</js> → {@link ChronoField#HOUR_OF_DAY}
-        *      <li><js>"YYYY-MM-DDTHH:MM"</js> → {@link 
ChronoField#MINUTE_OF_HOUR}
-        *      <li><js>"YYYY-MM-DDTHH:MM:SS"</js> → {@link 
ChronoField#SECOND_OF_MINUTE}
-        *      <li><js>"YYYY-MM-DDTHH:MM:SS.SSS"</js> → {@link 
ChronoField#MILLI_OF_SECOND}
-        * </ul>
-        *
-        * <p>
-        * Timezone information (Z, +HH:mm, -HH:mm) is preserved but doesn't 
affect the precision level.
-        * Invalid or unrecognized formats default to {@link 
ChronoField#MILLI_OF_SECOND}.
-        *
-        * <h5 class='section'>Examples:</h5>
-        * <p class='bjava'>
-        *      <jc>// Year precision</jc>
-        *      ChronoField <jv>precision1</jv> = 
DateUtils.<jsm>getPrecisionFromString</jsm>(<js>"2011"</js>);
-        *      <jc>// Returns ChronoField.YEAR</jc>
-        *
-        *      <jc>// Month precision</jc>
-        *      ChronoField <jv>precision2</jv> = 
DateUtils.<jsm>getPrecisionFromString</jsm>(<js>"2011-01"</js>);
-        *      <jc>// Returns ChronoField.MONTH_OF_YEAR</jc>
-        *
-        *      <jc>// Day precision</jc>
-        *      ChronoField <jv>precision3</jv> = 
DateUtils.<jsm>getPrecisionFromString</jsm>(<js>"2011-01-01"</js>);
-        *      <jc>// Returns ChronoField.DAY_OF_MONTH</jc>
-        *
-        *      <jc>// Hour precision with timezone</jc>
-        *      ChronoField <jv>precision4</jv> = 
DateUtils.<jsm>getPrecisionFromString</jsm>(<js>"2011-01-01T12Z"</js>);
-        *      <jc>// Returns ChronoField.HOUR_OF_DAY</jc>
-        *
-        *      <jc>// Millisecond precision</jc>
-        *      ChronoField <jv>precision5</jv> = 
DateUtils.<jsm>getPrecisionFromString</jsm>(<js>"2011-01-01T12:30:45.123"</js>);
-        *      <jc>// Returns ChronoField.MILLI_OF_SECOND</jc>
-        * </p>
-        *
-        * See Also: <a class="doclink" 
href="https://en.wikipedia.org/wiki/ISO_8601";>ISO 8601 - Wikipedia</a>
-        *
-        * @param seg The date/time string to analyze (can be null or empty)
-        * @return The ChronoField representing the precision level, or {@link 
ChronoField#MILLI_OF_SECOND} for invalid/empty strings
-        * @see ChronoField
-        */
-       public static ChronoField getPrecisionFromString(String seg) {
-               if (isEmpty(seg))
-                       return ChronoField.MILLI_OF_SECOND;
-
-               // States:
-               // S1: Looking for year digits (YYYY)
-               // S2: Found year, looking for - or T or end (YYYY)
-               // S3: Found -, looking for month digits (YYYY-MM)
-               // S4: Found month, looking for - or T or end (YYYY-MM)
-               // S5: Found -, looking for day digits (YYYY-MM-DD)
-               // S6: Found day, looking for T or end (YYYY-MM-DD)
-               // S7: Found T, looking for hour digits (YYYY-MM-DDTHH)
-               // S8: Found hour, looking for : or end (YYYY-MM-DDTHH)
-               // S9: Found :, looking for minute digits (YYYY-MM-DDTHH:MM)
-               // S10: Found minute, looking for : or end (YYYY-MM-DDTHH:MM)
-               // S11: Found :, looking for second digits (YYYY-MM-DDTHH:MM:SS)
-               // S12: Found second, looking for . or end (YYYY-MM-DDTHH:MM:SS)
-               // S13: Found ., looking for millisecond digits 
(YYYY-MM-DDTHH:MM:SS.SSS)
-               // S14: Found timezone (Z, +HH:mm, -HH:mm)
-
-               var state = S1;
-               var precision = ChronoField.YEAR; // Track precision as we go
-
-               for (var i = 0; i < seg.length(); i++) {
-                       var c = seg.charAt(i);
-
-                       if (state == S1) {
-                               // S1: Looking for year digits (YYYY)
-                               if (Character.isDigit(c)) {
-                                       state = S2;
-                               } else if (c == '-') {
-                                       state = S3;
-                                       precision = ChronoField.MONTH_OF_YEAR;
-                               } else if (c == 'T') {
-                                       state = S7;
-                                       precision = ChronoField.HOUR_OF_DAY;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision (YEAR)
-                               }
-                       } else if (state == S2) {
-                               // S2: Found year, looking for - or T or end 
(YYYY)
-                               if (c == '-') {
-                                       state = S3;
-                                       precision = ChronoField.MONTH_OF_YEAR;
-                               } else if (c == 'T') {
-                                       state = S7;
-                                       precision = ChronoField.HOUR_OF_DAY;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision (YEAR)
-                               }
-                       } else if (state == S3) {
-                               // S3: Found -, looking for month digits 
(YYYY-MM)
-                               if (Character.isDigit(c)) {
-                                       state = S4;
-                               } else if (c == '-') {
-                                       state = S5;
-                                       precision = ChronoField.DAY_OF_MONTH;
-                               } else if (c == 'T') {
-                                       state = S7;
-                                       precision = ChronoField.HOUR_OF_DAY;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision 
(MONTH_OF_YEAR)
-                               }
-                       } else if (state == S4) {
-                               // S4: Found month, looking for - or T or end 
(YYYY-MM)
-                               if (c == '-') {
-                                       state = S5;
-                                       precision = ChronoField.DAY_OF_MONTH;
-                               } else if (c == 'T') {
-                                       state = S7;
-                                       precision = ChronoField.HOUR_OF_DAY;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision 
(MONTH_OF_YEAR)
-                               }
-                       } else if (state == S5) {
-                               // S5: Found -, looking for day digits 
(YYYY-MM-DD)
-                               if (Character.isDigit(c)) {
-                                       state = S6;
-                               } else if (c == 'T') {
-                                       state = S7;
-                                       precision = ChronoField.HOUR_OF_DAY;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision (DAY_OF_MONTH)
-                               }
-                       } else if (state == S6) {
-                               // S6: Found day, looking for T or end 
(YYYY-MM-DD)
-                               if (c == 'T') {
-                                       state = S7;
-                                       precision = ChronoField.HOUR_OF_DAY;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision (DAY_OF_MONTH)
-                               }
-                       } else if (state == S7) {
-                               // S7: Found T, looking for hour digits 
(YYYY-MM-DDTHH)
-                               if (Character.isDigit(c)) {
-                                       state = S8;
-                               } else if (c == ':') {
-                                       state = S9;
-                                       precision = ChronoField.MINUTE_OF_HOUR;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision (HOUR_OF_DAY)
-                               }
-                       } else if (state == S8) {
-                               // S8: Found hour, looking for : or end 
(YYYY-MM-DDTHH)
-                               if (c == ':') {
-                                       state = S9;
-                                       precision = ChronoField.MINUTE_OF_HOUR;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision (HOUR_OF_DAY)
-                               }
-                       } else if (state == S9) {
-                               // S9: Found :, looking for minute digits 
(YYYY-MM-DDTHH:MM)
-                               if (Character.isDigit(c)) {
-                                       state = S10;
-                               } else if (c == ':') {
-                                       state = S11;
-                                       precision = 
ChronoField.SECOND_OF_MINUTE;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision 
(MINUTE_OF_HOUR)
-                               }
-                       } else if (state == S10) {
-                               // S10: Found minute, looking for : or end 
(YYYY-MM-DDTHH:MM)
-                               if (c == ':') {
-                                       state = S11;
-                                       precision = 
ChronoField.SECOND_OF_MINUTE;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision 
(MINUTE_OF_HOUR)
-                               }
-                       } else if (state == S11) {
-                               // S11: Found :, looking for second digits 
(YYYY-MM-DDTHH:MM:SS)
-                               if (Character.isDigit(c)) {
-                                       state = S12;
-                               } else if (c == '.') {
-                                       state = S13;
-                                       precision = ChronoField.MILLI_OF_SECOND;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision 
(SECOND_OF_MINUTE)
-                               }
-                       } else if (state == S12) {
-                               // S12: Found second, looking for . or end 
(YYYY-MM-DDTHH:MM:SS)
-                               if (c == '.') {
-                                       state = S13;
-                                       precision = ChronoField.MILLI_OF_SECOND;
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision 
(SECOND_OF_MINUTE)
-                               }
-                       } else if (state == S13) {
-                               // S13: Found ., looking for millisecond digits 
(YYYY-MM-DDTHH:MM:SS.SSS)
-                               if (Character.isDigit(c)) {
-                                       // Continue reading millisecond digits
-                               } else if (c == 'Z' || c == '+' || c == '-') {
-                                       state = S14;
-                                       // Keep current precision 
(MILLI_OF_SECOND)
-                               }
-                       } else if (state == S14) {
-                               // S14: Found timezone (Z, +HH:mm, -HH:mm) - 
precision already determined
-                               // Just continue reading timezone characters
-                       }
-               }
-
-               return precision;
-       }
 
        // 
================================================================================================================
        // ChronoField/ChronoUnit/Calendar conversion utilities
        // 
================================================================================================================
 
-       /**
-        * Converts a ChronoField to its corresponding Calendar field constant.
-        *
-        * <p>
-        * This method provides a mapping from modern ChronoField values to 
legacy
-        * Calendar field constants for use with Calendar.add() and similar 
methods.
-        *
-        * @param field The ChronoField to convert
-        * @return The corresponding Calendar field constant
-        * @see ChronoField
-        * @see Calendar
-        */
-       public static int toCalendarField(ChronoField field) {
-               return switch (field) {
-                       case YEAR -> Calendar.YEAR;
-                       case MONTH_OF_YEAR -> Calendar.MONTH;
-                       case DAY_OF_MONTH -> Calendar.DAY_OF_MONTH;
-                       case HOUR_OF_DAY -> Calendar.HOUR_OF_DAY;
-                       case MINUTE_OF_HOUR -> Calendar.MINUTE;
-                       case SECOND_OF_MINUTE -> Calendar.SECOND;
-                       case MILLI_OF_SECOND -> Calendar.MILLISECOND;
-                       default -> Calendar.MILLISECOND;
-               };
-       }
-
-       /**
-        * Converts a ChronoUnit to its corresponding ChronoField.
-        *
-        * <p>
-        * This method provides a mapping from time units to date/time fields.
-        * Not all ChronoUnit values have direct ChronoField equivalents.
-        *
-        * @param unit The ChronoUnit to convert
-        * @return The corresponding ChronoField, or null if no direct mapping 
exists
-        * @see ChronoUnit
-        * @see ChronoField
-        */
-       public static ChronoField toChronoField(ChronoUnit unit) {
-               return switch (unit) {
-                       case YEARS -> ChronoField.YEAR;
-                       case MONTHS -> ChronoField.MONTH_OF_YEAR;
-                       case DAYS -> ChronoField.DAY_OF_MONTH;
-                       case HOURS -> ChronoField.HOUR_OF_DAY;
-                       case MINUTES -> ChronoField.MINUTE_OF_HOUR;
-                       case SECONDS -> ChronoField.SECOND_OF_MINUTE;
-                       case MILLIS -> ChronoField.MILLI_OF_SECOND;
-                       default -> null;
-               };
-       }
-
-       /**
-        * Converts a ChronoField to its corresponding ChronoUnit.
-        *
-        * <p>
-        * This method provides a mapping from date/time fields to time units.
-        * Not all ChronoField values have direct ChronoUnit equivalents.
-        *
-        * @param field The ChronoField to convert
-        * @return The corresponding ChronoUnit, or null if no direct mapping 
exists
-        * @see ChronoField
-        * @see ChronoUnit
-        */
-       public static ChronoUnit toChronoUnit(ChronoField field) {
-               return switch (field) {
-                       case YEAR -> ChronoUnit.YEARS;
-                       case MONTH_OF_YEAR -> ChronoUnit.MONTHS;
-                       case DAY_OF_MONTH -> ChronoUnit.DAYS;
-                       case HOUR_OF_DAY -> ChronoUnit.HOURS;
-                       case MINUTE_OF_HOUR -> ChronoUnit.MINUTES;
-                       case SECOND_OF_MINUTE -> ChronoUnit.SECONDS;
-                       case MILLI_OF_SECOND -> ChronoUnit.MILLIS;
-                       default -> null;
-               };
-       }
-
-       /**
-        * Converts a Calendar object to an ISO8601 formatted string.
-        *
-        * <p>
-        * This method formats a Calendar object into a standard ISO8601 
date/time string
-        * with timezone information. The output format follows the pattern:
-        * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code>
-        *
-        * <p>
-        * The method preserves the timezone information from the Calendar 
object and
-        * formats it according to ISO8601 standards, including the timezone 
offset.
-        *
-        * <h5 class='section'>Examples:</h5>
-        * <p class='bjava'>
-        *      <jc>// Create a Calendar with a specific timezone</jc>
-        *      Calendar <jv>cal</jv> = 
Calendar.getInstance(TimeZone.getTimeZone(<js>"America/New_York"</js>));
-        *      <jv>cal</jv>.set(2024, Calendar.JANUARY, 15, 14, 30, 45);
-        *      <jv>cal</jv>.set(Calendar.MILLISECOND, 123);
-        *
-        *      <jc>// Convert to ISO8601 string</jc>
-        *      String <jv>iso8601</jv> = 
DateUtils.<jsm>toIso8601</jsm>(<jv>cal</jv>);
-        *      <jc>// Result: "2024-01-15T14:30:45-05:00" (or -04:00 during 
DST)</jc>
-        *
-        *      <jc>// UTC timezone example</jc>
-        *      Calendar <jv>utcCal</jv> = 
Calendar.getInstance(TimeZone.getTimeZone(<js>"UTC"</js>));
-        *      <jv>utcCal</jv>.set(2024, Calendar.JANUARY, 15, 19, 30, 45);
-        *      String <jv>utcIso</jv> = 
DateUtils.<jsm>toIso8601</jsm>(<jv>utcCal</jv>);
-        *      <jc>// Result: "2024-01-15T19:30:45Z"</jc>
-        * </p>
-        *
-        * <h5 class='section'>Format Details:</h5>
-        * <ul>
-        *      <li><c>yyyy</c> - 4-digit year
-        *      <li><c>MM</c> - 2-digit month (01-12)
-        *      <li><c>dd</c> - 2-digit day of month (01-31)
-        *      <li><c>T</c> - Literal 'T' separator between date and time
-        *      <li><c>HH</c> - 2-digit hour in 24-hour format (00-23)
-        *      <li><c>mm</c> - 2-digit minute (00-59)
-        *      <li><c>ss</c> - 2-digit second (00-59)
-        *      <li><c>XXX</c> - Timezone offset (+HH:mm, -HH:mm, or Z for UTC)
-        * </ul>
-        *
-        * <h5 class='section'>Timezone Handling:</h5>
-        * <p>
-        * The method uses the Calendar's timezone to determine the appropriate 
offset.
-        * UTC timezones are represented as 'Z', while other timezones show 
their
-        * offset from UTC (e.g., -05:00 for EST, -04:00 for EDT).
-        * </p>
-        *
-        * See Also:  <a class="doclink" 
href="https://en.wikipedia.org/wiki/ISO_8601";>ISO 8601 - Wikipedia</a>
-        *
-        * @param c The Calendar object to convert (cannot be null)
-        * @return ISO8601 formatted string representation of the Calendar
-        * @throws NullPointerException if the Calendar parameter is null
-        * @see SimpleDateFormat
-        * @see Calendar#getTimeZone()
-        */
-       public static String toIso8601(Calendar c) {
-               var sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
-               sdf.setTimeZone(c.getTimeZone());
-               return sdf.format(c.getTime());
-       }
 
        /**
         * Pads out an ISO8601 string so that it can be parsed using {@link 
DatatypeConverter#parseDateTime(String)}.
@@ -807,12 +323,53 @@ public class DateUtils {
        }
 
        /**
-        * Converts a calendar to a {@link ZonedDateTime}.
+        * Parses an ISO8601 string into a calendar.
+        *
+        * <p>
+        * TODO-90: Investigate whether this helper can be removed in favor of 
java.time parsing (see TODO.md).
+        *
+        * <p>
+        * Supports any of the following formats:
+        * <br><c>yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, 
yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS</c>
         *
-        * @param c The calendar to convert.
-        * @return An {@link Optional} containing the {@link ZonedDateTime}, or 
empty if the input was <jk>null</jk>.
+        * @param date The date string.
+        * @return The parsed calendar.
+        * @throws IllegalArgumentException Value was not a valid date.
         */
-       public static Optional<ZonedDateTime> toZonedDateTime(Calendar c) {
-               return 
opt(c).map(GregorianCalendar.class::cast).map(GregorianCalendar::toZonedDateTime);
+       public static Calendar parseIsoCalendar(String date) throws 
IllegalArgumentException {
+               if (StringUtils.isEmpty(date))
+                       return null;
+               date = date.trim().replace(' ', 'T');  // Convert to 'standard' 
ISO8601
+               if (date.indexOf(',') != -1)  // Trim milliseconds
+                       date = date.substring(0, date.indexOf(','));
+               if (date.matches("\\d{4}"))
+                       date += "-01-01T00:00:00";
+               else if (date.matches("\\d{4}\\-\\d{2}"))
+                       date += "-01T00:00:00";
+               else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}"))
+                       date += "T00:00:00";
+               else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}"))
+                       date += ":00:00";
+               else if 
(date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}\\:\\d{2}"))
+                       date += ":00";
+               return fromIso8601Calendar(date);
+       }
+
+       /**
+        * Parses an ISO8601 string into a date.
+        *
+        * <p>
+        * Supports any of the following formats:
+        * <br><c>yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, 
yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS</c>
+        *
+        * @param date The date string.
+        * @return The parsed date.
+        * @throws IllegalArgumentException Value was not a valid date.
+        */
+       public static Date parseIsoDate(String date) throws 
IllegalArgumentException {
+               if (StringUtils.isEmpty(date))
+                       return null;
+               return parseIsoCalendar(date).getTime();  // NOSONAR - NPE not 
possible.
        }
+
 }
\ No newline at end of file
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 60b72acff6..0f6dea9260 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
@@ -17,6 +17,8 @@
 package org.apache.juneau.commons.utils;
 
 import static org.apache.juneau.commons.utils.DateUtils.*;
+import static org.apache.juneau.commons.utils.StateEnum.*;
+import static org.apache.juneau.commons.utils.StringUtils.*;
 import static org.apache.juneau.commons.utils.ThrowableUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 
@@ -58,7 +60,7 @@ public class GranularZonedDateTime {
         *
         * <p>
         * This method uses {@link DateUtils#fromIso8601(String)} for parsing 
and
-        * {@link DateUtils#getPrecisionFromString(String)} for determining 
precision.
+        * determines precision based on the input string format.
         *
         * @param seg The string segment to parse.
         * @return A GranularZonedDateTime representing the parsed timestamp.
@@ -128,7 +130,6 @@ public class GranularZonedDateTime {
         * @return A new GranularZonedDateTime with the rolled value.
         */
        public GranularZonedDateTime roll(ChronoField field, int amount) {
-               // Use DateUtils utility method to convert ChronoField to 
ChronoUnit
                ChronoUnit unit = toChronoUnit(field);
                if (nn(unit)) {
                        ZonedDateTime newZdt = zdt.plus(amount, unit);
@@ -146,4 +147,229 @@ public class GranularZonedDateTime {
        public GranularZonedDateTime roll(int amount) {
                return roll(precision, amount);
        }
+
+       /**
+        * Converts a ChronoField to its corresponding ChronoUnit.
+        *
+        * <p>
+        * This method provides a mapping from date/time fields to time units.
+        * Not all ChronoField values have direct ChronoUnit equivalents.
+        *
+        * @param field The ChronoField to convert
+        * @return The corresponding ChronoUnit, or null if no direct mapping 
exists
+        */
+       private static ChronoUnit toChronoUnit(ChronoField field) {
+               return switch (field) {
+                       case YEAR -> ChronoUnit.YEARS;
+                       case MONTH_OF_YEAR -> ChronoUnit.MONTHS;
+                       case DAY_OF_MONTH -> ChronoUnit.DAYS;
+                       case HOUR_OF_DAY -> ChronoUnit.HOURS;
+                       case MINUTE_OF_HOUR -> ChronoUnit.MINUTES;
+                       case SECOND_OF_MINUTE -> ChronoUnit.SECONDS;
+                       case MILLI_OF_SECOND -> ChronoUnit.MILLIS;
+                       default -> null;
+               };
+       }
+
+       /**
+        * Determines the precision level of an ISO8601 date/time string using 
a state machine.
+        *
+        * <p>
+        * This method analyzes the structure of a date/time string to 
determine the finest level of precision
+        * represented. It uses a state machine to parse the string character 
by character, tracking the precision
+        * level as it encounters different components.
+        *
+        * <p>
+        * The method supports the following ISO8601 formats:
+        * <ul>
+        *      <li><js>"YYYY"</js> → {@link ChronoField#YEAR}
+        *      <li><js>"YYYY-MM"</js> → {@link ChronoField#MONTH_OF_YEAR}
+        *      <li><js>"YYYY-MM-DD"</js> → {@link ChronoField#DAY_OF_MONTH}
+        *      <li><js>"YYYY-MM-DDTHH"</js> → {@link ChronoField#HOUR_OF_DAY}
+        *      <li><js>"YYYY-MM-DDTHH:MM"</js> → {@link 
ChronoField#MINUTE_OF_HOUR}
+        *      <li><js>"YYYY-MM-DDTHH:MM:SS"</js> → {@link 
ChronoField#SECOND_OF_MINUTE}
+        *      <li><js>"YYYY-MM-DDTHH:MM:SS.SSS"</js> → {@link 
ChronoField#MILLI_OF_SECOND}
+        * </ul>
+        *
+        * <p>
+        * Timezone information (Z, +HH:mm, -HH:mm) is preserved but doesn't 
affect the precision level.
+        * Invalid or unrecognized formats default to {@link 
ChronoField#MILLI_OF_SECOND}.
+        *
+        * @param seg The date/time string to analyze (can be null or empty)
+        * @return The ChronoField representing the precision level, or {@link 
ChronoField#MILLI_OF_SECOND} for invalid/empty strings
+        */
+       private static ChronoField getPrecisionFromString(String seg) {
+               if (isEmpty(seg))
+                       return ChronoField.MILLI_OF_SECOND;
+
+               // States:
+               // S1: Looking for year digits (YYYY)
+               // S2: Found year, looking for - or T or end (YYYY)
+               // S3: Found -, looking for month digits (YYYY-MM)
+               // S4: Found month, looking for - or T or end (YYYY-MM)
+               // S5: Found -, looking for day digits (YYYY-MM-DD)
+               // S6: Found day, looking for T or end (YYYY-MM-DD)
+               // S7: Found T, looking for hour digits (YYYY-MM-DDTHH)
+               // S8: Found hour, looking for : or end (YYYY-MM-DDTHH)
+               // S9: Found :, looking for minute digits (YYYY-MM-DDTHH:MM)
+               // S10: Found minute, looking for : or end (YYYY-MM-DDTHH:MM)
+               // S11: Found :, looking for second digits (YYYY-MM-DDTHH:MM:SS)
+               // S12: Found second, looking for . or end (YYYY-MM-DDTHH:MM:SS)
+               // S13: Found ., looking for millisecond digits 
(YYYY-MM-DDTHH:MM:SS.SSS)
+               // S14: Found timezone (Z, +HH:mm, -HH:mm)
+
+               var state = S1;
+               var precision = ChronoField.YEAR; // Track precision as we go
+
+               for (var i = 0; i < seg.length(); i++) {
+                       var c = seg.charAt(i);
+
+                       if (state == S1) {
+                               // S1: Looking for year digits (YYYY)
+                               if (Character.isDigit(c)) {
+                                       state = S2;
+                               } else if (c == '-') {
+                                       state = S3;
+                                       precision = ChronoField.MONTH_OF_YEAR;
+                               } else if (c == 'T') {
+                                       state = S7;
+                                       precision = ChronoField.HOUR_OF_DAY;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision (YEAR)
+                               }
+                       } else if (state == S2) {
+                               // S2: Found year, looking for - or T or end 
(YYYY)
+                               if (c == '-') {
+                                       state = S3;
+                                       precision = ChronoField.MONTH_OF_YEAR;
+                               } else if (c == 'T') {
+                                       state = S7;
+                                       precision = ChronoField.HOUR_OF_DAY;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision (YEAR)
+                               }
+                       } else if (state == S3) {
+                               // S3: Found -, looking for month digits 
(YYYY-MM)
+                               if (Character.isDigit(c)) {
+                                       state = S4;
+                               } else if (c == '-') {
+                                       state = S5;
+                                       precision = ChronoField.DAY_OF_MONTH;
+                               } else if (c == 'T') {
+                                       state = S7;
+                                       precision = ChronoField.HOUR_OF_DAY;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision 
(MONTH_OF_YEAR)
+                               }
+                       } else if (state == S4) {
+                               // S4: Found month, looking for - or T or end 
(YYYY-MM)
+                               if (c == '-') {
+                                       state = S5;
+                                       precision = ChronoField.DAY_OF_MONTH;
+                               } else if (c == 'T') {
+                                       state = S7;
+                                       precision = ChronoField.HOUR_OF_DAY;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision 
(MONTH_OF_YEAR)
+                               }
+                       } else if (state == S5) {
+                               // S5: Found -, looking for day digits 
(YYYY-MM-DD)
+                               if (Character.isDigit(c)) {
+                                       state = S6;
+                               } else if (c == 'T') {
+                                       state = S7;
+                                       precision = ChronoField.HOUR_OF_DAY;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision (DAY_OF_MONTH)
+                               }
+                       } else if (state == S6) {
+                               // S6: Found day, looking for T or end 
(YYYY-MM-DD)
+                               if (c == 'T') {
+                                       state = S7;
+                                       precision = ChronoField.HOUR_OF_DAY;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision (DAY_OF_MONTH)
+                               }
+                       } else if (state == S7) {
+                               // S7: Found T, looking for hour digits 
(YYYY-MM-DDTHH)
+                               if (Character.isDigit(c)) {
+                                       state = S8;
+                               } else if (c == ':') {
+                                       state = S9;
+                                       precision = ChronoField.MINUTE_OF_HOUR;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision (HOUR_OF_DAY)
+                               }
+                       } else if (state == S8) {
+                               // S8: Found hour, looking for : or end 
(YYYY-MM-DDTHH)
+                               if (c == ':') {
+                                       state = S9;
+                                       precision = ChronoField.MINUTE_OF_HOUR;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision (HOUR_OF_DAY)
+                               }
+                       } else if (state == S9) {
+                               // S9: Found :, looking for minute digits 
(YYYY-MM-DDTHH:MM)
+                               if (Character.isDigit(c)) {
+                                       state = S10;
+                               } else if (c == ':') {
+                                       state = S11;
+                                       precision = 
ChronoField.SECOND_OF_MINUTE;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision 
(MINUTE_OF_HOUR)
+                               }
+                       } else if (state == S10) {
+                               // S10: Found minute, looking for : or end 
(YYYY-MM-DDTHH:MM)
+                               if (c == ':') {
+                                       state = S11;
+                                       precision = 
ChronoField.SECOND_OF_MINUTE;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision 
(MINUTE_OF_HOUR)
+                               }
+                       } else if (state == S11) {
+                               // S11: Found :, looking for second digits 
(YYYY-MM-DDTHH:MM:SS)
+                               if (Character.isDigit(c)) {
+                                       state = S12;
+                               } else if (c == '.') {
+                                       state = S13;
+                                       precision = ChronoField.MILLI_OF_SECOND;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision 
(SECOND_OF_MINUTE)
+                               }
+                       } else if (state == S12) {
+                               // S12: Found second, looking for . or end 
(YYYY-MM-DDTHH:MM:SS)
+                               if (c == '.') {
+                                       state = S13;
+                                       precision = ChronoField.MILLI_OF_SECOND;
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision 
(SECOND_OF_MINUTE)
+                               }
+                       } else if (state == S13) {
+                               // S13: Found ., looking for millisecond digits 
(YYYY-MM-DDTHH:MM:SS.SSS)
+                               if (Character.isDigit(c)) {
+                                       // Continue reading millisecond digits
+                               } else if (c == 'Z' || c == '+' || c == '-') {
+                                       state = S14;
+                                       // Keep current precision 
(MILLI_OF_SECOND)
+                               }
+                       } else if (state == S14) {
+                               // S14: Found timezone (Z, +HH:mm, -HH:mm) - 
precision already determined
+                               // Just continue reading timezone characters
+                       }
+               }
+
+               return precision;
+       }
 }
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/StringUtils.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/StringUtils.java
index 941d0878bc..2500475717 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/StringUtils.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/StringUtils.java
@@ -5075,56 +5075,6 @@ public class StringUtils {
                return Integer.decode(s.substring(0, s.length() - 1).trim()) * 
m;  // NOSONAR - NPE not possible here.
        }
 
-       /**
-        * Parses an ISO8601 string into a calendar.
-        *
-        * <p>
-        * TODO-90: Investigate whether this helper can be removed in favor of 
java.time parsing (see TODO.md).
-        *
-        * <p>
-        * Supports any of the following formats:
-        * <br><c>yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, 
yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS</c>
-        *
-        * @param date The date string.
-        * @return The parsed calendar.
-        * @throws IllegalArgumentException Value was not a valid date.
-        */
-       public static Calendar parseIsoCalendar(String date) throws 
IllegalArgumentException {
-               if (isEmpty(date))
-                       return null;
-               date = date.trim().replace(' ', 'T');  // Convert to 'standard' 
ISO8601
-               if (date.indexOf(',') != -1)  // Trim milliseconds
-                       date = date.substring(0, date.indexOf(','));
-               if (date.matches("\\d{4}"))
-                       date += "-01-01T00:00:00";
-               else if (date.matches("\\d{4}\\-\\d{2}"))
-                       date += "-01T00:00:00";
-               else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}"))
-                       date += "T00:00:00";
-               else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}"))
-                       date += ":00:00";
-               else if 
(date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}\\:\\d{2}"))
-                       date += ":00";
-               return DateUtils.fromIso8601Calendar(date);
-       }
-
-       /**
-        * Parses an ISO8601 string into a date.
-        *
-        * <p>
-        * Supports any of the following formats:
-        * <br><c>yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, 
yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS</c>
-        *
-        * @param date The date string.
-        * @return The parsed date.
-        * @throws IllegalArgumentException Value was not a valid date.
-        */
-       public static Date parseIsoDate(String date) throws 
IllegalArgumentException {
-               if (isEmpty(date))
-                       return null;
-               return parseIsoCalendar(date).getTime();  // NOSONAR - NPE not 
possible.
-       }
-
        /**
         * Same as {@link Long#parseLong(String)} but removes any underscore 
characters first.
         *
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
index bffa714a51..7c1e6d38c8 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
@@ -280,7 +280,7 @@ public class OpenApiParserSession extends UonParserSession {
                                        if (f == BYTE)
                                                return toType(base64Decode(in), 
type);
                                        if (f == DATE || f == DATE_TIME)
-                                               return 
toType(parseIsoCalendar(in), type);
+                                               return 
toType(DateUtils.parseIsoCalendar(in), type);
                                        if (f == BINARY)
                                                return toType(fromHex(in), 
type);
                                        if (f == BINARY_SPACED)
diff --git 
a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogsResource.java
 
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogsResource.java
index a9610b0a86..601695a60c 100644
--- 
a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogsResource.java
+++ 
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogsResource.java
@@ -25,6 +25,7 @@ import java.util.*;
 
 import org.apache.juneau.annotation.*;
 import org.apache.juneau.bean.*;
+import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.config.*;
 import org.apache.juneau.html.annotation.*;
 import org.apache.juneau.http.annotation.*;
@@ -226,8 +227,8 @@ public class LogsResource extends BasicRestServlet {
 
                var f = getFile(path);
 
-               var startDate = parseIsoDate(start);
-               var endDate = parseIsoDate(end);
+               var startDate = DateUtils.parseIsoDate(start);
+               var endDate = DateUtils.parseIsoDate(end);
 
                if (! highlight) {
                        var o = getReader(f, startDate, endDate, thread, 
loggers, severity);
@@ -296,8 +297,8 @@ public class LogsResource extends BasicRestServlet {
                var f = getFile(path);
                req.setAttribute("fullPath", f.getAbsolutePath());
 
-               var startDate = parseIsoDate(start);
-               var endDate = parseIsoDate(end);
+               var startDate = DateUtils.parseIsoDate(start);
+               var endDate = DateUtils.parseIsoDate(end);
 
                return getLogParser(f, startDate, endDate, thread, loggers, 
severity);
        }
diff --git 
a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/MockServletResponse.java
 
b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/MockServletResponse.java
index d101f3e408..5a932e5939 100644
--- 
a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/MockServletResponse.java
+++ 
b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/MockServletResponse.java
@@ -17,10 +17,12 @@
 package org.apache.juneau.rest.mock;
 
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
-import static org.apache.juneau.commons.utils.DateUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 
 import java.io.*;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 
 import org.apache.juneau.rest.util.*;
@@ -60,7 +62,10 @@ public class MockServletResponse implements 
HttpServletResponse {
 
        @Override /* Overridden from HttpServletResponse */
        public void addDateHeader(String name, long date) {
-               headerMap.put(name, a(formatDate(new Date(date), 
PATTERN_RFC1123)));
+               Instant instant = Instant.ofEpochMilli(date);
+               DateTimeFormatter formatter = 
DateTimeFormatter.RFC_1123_DATE_TIME
+                       .withZone(ZoneId.of("GMT"));
+               headerMap.put(name, a(formatter.format(instant)));
        }
 
        @Override /* Overridden from HttpServletResponse */
@@ -205,7 +210,10 @@ public class MockServletResponse implements 
HttpServletResponse {
 
        @Override /* Overridden from HttpServletResponse */
        public void setDateHeader(String name, long date) {
-               headerMap.put(name, a(formatDate(new Date(date), 
PATTERN_RFC1123)));
+               Instant instant = Instant.ofEpochMilli(date);
+               DateTimeFormatter formatter = 
DateTimeFormatter.RFC_1123_DATE_TIME
+                       .withZone(ZoneId.of("GMT"));
+               headerMap.put(name, a(formatter.format(instant)));
        }
 
        @Override /* Overridden from HttpServletResponse */
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/utils/DateUtils_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/utils/DateUtils_Test.java
index a77d758fc3..5c2665a108 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/utils/DateUtils_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/utils/DateUtils_Test.java
@@ -45,223 +45,16 @@ class DateUtils_Test extends TestBase {
        }
 
        
//-----------------------------------------------------------------------------------------------------------------
-       // Test getPrecisionFromString method (state machine implementation)
+       // Test getPrecisionFromString method (now in GranularZonedDateTime)
        
//-----------------------------------------------------------------------------------------------------------------
-
-       static class A_getPrecisionFromString {
-
-               private static final Input[] INPUT = {
-                       /* 01 */ input(1, "2011", ChronoField.YEAR),
-                       /* 02 */ input(2, "2024", ChronoField.YEAR),
-                       /* 03 */ input(3, "1999", ChronoField.YEAR),
-                       /* 04 */ input(4, "2011-01", MONTH_OF_YEAR),
-                       /* 05 */ input(5, "2024-12", MONTH_OF_YEAR),
-                       /* 06 */ input(6, "1999-06", MONTH_OF_YEAR),
-                       /* 07 */ input(7, "2011-01-01", 
ChronoField.DAY_OF_MONTH),
-                       /* 08 */ input(8, "2024-12-31", 
ChronoField.DAY_OF_MONTH),
-                       /* 09 */ input(9, "1999-06-15", 
ChronoField.DAY_OF_MONTH),
-                       /* 10 */ input(10, "2011-01-01T12", 
ChronoField.HOUR_OF_DAY),
-                       /* 11 */ input(11, "2024-12-31T23", 
ChronoField.HOUR_OF_DAY),
-                       /* 12 */ input(12, "1999-06-15T00", 
ChronoField.HOUR_OF_DAY),
-                       /* 13 */ input(13, "2011-01-01T12:30", MINUTE_OF_HOUR),
-                       /* 14 */ input(14, "2024-12-31T23:59", MINUTE_OF_HOUR),
-                       /* 15 */ input(15, "1999-06-15T00:00", MINUTE_OF_HOUR),
-                       /* 16 */ input(16, "2011-01-01T12:30:45", 
SECOND_OF_MINUTE),
-                       /* 17 */ input(17, "2024-12-31T23:59:59", 
SECOND_OF_MINUTE),
-                       /* 18 */ input(18, "1999-06-15T00:00:00", 
SECOND_OF_MINUTE),
-                       /* 19 */ input(19, "2011-01-01T12:30:45.123", 
MILLI_OF_SECOND),
-                       /* 20 */ input(20, "2024-12-31T23:59:59.999", 
MILLI_OF_SECOND),
-                       /* 21 */ input(21, "1999-06-15T00:00:00.000", 
MILLI_OF_SECOND),
-                       /* 22 */ input(22, "0000", ChronoField.YEAR),
-                       /* 23 */ input(23, "9999", ChronoField.YEAR),
-                       /* 24 */ input(24, "0000-01", MONTH_OF_YEAR),
-                       /* 25 */ input(25, "9999-12", MONTH_OF_YEAR),
-                       /* 26 */ input(26, "0000-01-01", 
ChronoField.DAY_OF_MONTH),
-                       /* 27 */ input(27, "9999-12-31", 
ChronoField.DAY_OF_MONTH),
-                       /* 28 */ input(28, "", MILLI_OF_SECOND),
-                       /* 35 */ input(35, "2011Z", ChronoField.YEAR),
-                       /* 36 */ input(36, "2011-01Z", MONTH_OF_YEAR),
-                       /* 37 */ input(37, "2011-01-01Z", 
ChronoField.DAY_OF_MONTH),
-                       /* 38 */ input(38, "2011-01-01T12Z", 
ChronoField.HOUR_OF_DAY),
-                       /* 39 */ input(39, "2011-01-01T12:30Z", MINUTE_OF_HOUR),
-                       /* 40 */ input(40, "2011-01-01T12:30:45Z", 
SECOND_OF_MINUTE),
-                       /* 41 */ input(41, "2011-01-01T12:30:45.123Z", 
MILLI_OF_SECOND)
-               };
-
-               private static Input input(int index, String dateString, 
ChronoField expectedPrecision) {
-                       return new Input(index, dateString, expectedPrecision);
-               }
-
-               private static class Input {
-                       final String dateString;
-                       final ChronoField expectedPrecision;
-
-                       public Input(int index, String dateString, ChronoField 
expectedPrecision) {
-                               this.dateString = dateString;
-                               this.expectedPrecision = expectedPrecision;
-                       }
-               }
-
-               static Input[] input() {
-                       return INPUT;
-               }
-
-               @ParameterizedTest
-               @MethodSource("input")
-               void a01_basic(Input input) {
-                       assertEquals(input.expectedPrecision, 
getPrecisionFromString(input.dateString));
-               }
-       }
+       // Note: getPrecisionFromString has been moved to GranularZonedDateTime 
as a private method.
+       // Tests for this functionality are now in GranularZonedDateTime_Test 
via the parse() method.
 
        
//-----------------------------------------------------------------------------------------------------------------
-       // toChronoField(ChronoUnit) tests
+       // toChronoUnit(ChronoField) tests (now in GranularZonedDateTime)
        
//-----------------------------------------------------------------------------------------------------------------
-
-       static class D_toChronoField {
-
-               private static final Input[] INPUT = {
-                       /* 01 */ input(1, YEARS, ChronoField.YEAR),
-                       /* 02 */ input(2, MONTHS, MONTH_OF_YEAR),
-                       /* 03 */ input(3, DAYS, ChronoField.DAY_OF_MONTH),
-                       /* 04 */ input(4, HOURS, ChronoField.HOUR_OF_DAY),
-                       /* 05 */ input(5, MINUTES, MINUTE_OF_HOUR),
-                       /* 06 */ input(6, SECONDS, SECOND_OF_MINUTE),
-                       /* 07 */ input(7, MILLIS, MILLI_OF_SECOND),
-                       /* 08 */ input(8, NANOS, null),
-                       /* 09 */ input(9, MICROS, null),
-                       /* 10 */ input(10, WEEKS, null),
-                       /* 11 */ input(11, DECADES, null),
-                       /* 12 */ input(12, CENTURIES, null),
-                       /* 13 */ input(13, MILLENNIA, null),
-                       /* 14 */ input(14, ERAS, null)
-               };
-
-               private static Input input(int index, ChronoUnit unit, 
ChronoField expectedField) {
-                       return new Input(index, unit, expectedField);
-               }
-
-               private static class Input {
-                       final int index;
-                       final ChronoUnit unit;
-                       final ChronoField expectedField;
-
-                       public Input(int index, ChronoUnit unit, ChronoField 
expectedField) {
-                               this.index = index;
-                               this.unit = unit;
-                               this.expectedField = expectedField;
-                       }
-               }
-
-               static Input[] input() {
-                       return INPUT;
-               }
-
-               @ParameterizedTest
-               @MethodSource("input")
-               void d01_toChronoField(Input input) {
-                       ChronoField result = toChronoField(input.unit);
-                       assertEquals(input.expectedField, result, "Test " + 
input.index + ": " + input.unit);
-               }
-       }
-
-       
//-----------------------------------------------------------------------------------------------------------------
-       // toChronoUnit(ChronoField) tests
-       
//-----------------------------------------------------------------------------------------------------------------
-
-       static class E_toChronoUnit {
-
-               private static final Input[] INPUT = {
-                       /* 01 */ input(1, ChronoField.YEAR, YEARS),
-                       /* 02 */ input(2, MONTH_OF_YEAR, MONTHS),
-                       /* 03 */ input(3, ChronoField.DAY_OF_MONTH, DAYS),
-                       /* 04 */ input(4, ChronoField.HOUR_OF_DAY, HOURS),
-                       /* 05 */ input(5, MINUTE_OF_HOUR, MINUTES),
-                       /* 06 */ input(6, SECOND_OF_MINUTE, SECONDS),
-                       /* 07 */ input(7, MILLI_OF_SECOND, MILLIS),
-                       /* 08 */ input(8, ChronoField.DAY_OF_WEEK, null),
-                       /* 09 */ input(9, ChronoField.DAY_OF_YEAR, null),
-                       /* 11 */ input(11, ALIGNED_DAY_OF_WEEK_IN_MONTH, null),
-                       /* 12 */ input(12, ALIGNED_WEEK_OF_MONTH, null),
-                       /* 13 */ input(13, ALIGNED_WEEK_OF_YEAR, null),
-                       /* 14 */ input(14, NANO_OF_SECOND, null),
-                       /* 15 */ input(15, MICRO_OF_SECOND, null)
-               };
-
-               private static Input input(int index, ChronoField field, 
ChronoUnit expectedUnit) {
-                       return new Input(index, field, expectedUnit);
-               }
-
-               private static class Input {
-                       final int index;
-                       final ChronoField field;
-                       final ChronoUnit expectedUnit;
-
-                       public Input(int index, ChronoField field, ChronoUnit 
expectedUnit) {
-                               this.index = index;
-                               this.field = field;
-                               this.expectedUnit = expectedUnit;
-                       }
-               }
-
-               static Input[] input() {
-                       return INPUT;
-               }
-
-               @ParameterizedTest
-               @MethodSource("input")
-               void e01_toChronoUnit(Input input) {
-                       ChronoUnit result = toChronoUnit(input.field);
-                       assertEquals(input.expectedUnit, result, "Test " + 
input.index + ": " + input.field);
-               }
-       }
-
-       
//-----------------------------------------------------------------------------------------------------------------
-       // toCalendarField(ChronoField) tests
-       
//-----------------------------------------------------------------------------------------------------------------
-
-       static class F_toCalendarField {
-
-               private static final Input[] INPUT = {
-                       /* 01 */ input(1, ChronoField.YEAR, Calendar.YEAR),
-                       /* 02 */ input(2, MONTH_OF_YEAR, MONTH),
-                       /* 03 */ input(3, ChronoField.DAY_OF_MONTH, 
Calendar.DAY_OF_MONTH),
-                       /* 04 */ input(4, ChronoField.HOUR_OF_DAY, 
Calendar.HOUR_OF_DAY),
-                       /* 05 */ input(5, MINUTE_OF_HOUR, MINUTE),
-                       /* 06 */ input(6, SECOND_OF_MINUTE, SECOND),
-                       /* 07 */ input(7, MILLI_OF_SECOND, MILLISECOND),
-                       /* 08 */ input(8, ChronoField.DAY_OF_WEEK, 
MILLISECOND), // Should default to MILLISECOND
-                       /* 09 */ input(9, ChronoField.DAY_OF_YEAR, 
MILLISECOND), // Should default to MILLISECOND
-                       /* 11 */ input(11, NANO_OF_SECOND, MILLISECOND), // 
Should default to MILLISECOND
-                       /* 12 */ input(12, MICRO_OF_SECOND, MILLISECOND) // 
Should default to MILLISECOND
-               };
-
-               private static Input input(int index, ChronoField field, int 
expectedCalendarField) {
-                       return new Input(index, field, expectedCalendarField);
-               }
-
-               private static class Input {
-                       final int index;
-                       final ChronoField field;
-                       final int expectedCalendarField;
-
-                       public Input(int index, ChronoField field, int 
expectedCalendarField) {
-                               this.index = index;
-                               this.field = field;
-                               this.expectedCalendarField = 
expectedCalendarField;
-                       }
-               }
-
-               static Input[] input() {
-                       return INPUT;
-               }
-
-               @ParameterizedTest
-               @MethodSource("input")
-               void f01_toCalendarField(Input input) {
-                       int result = toCalendarField(input.field);
-                       assertEquals(input.expectedCalendarField, result, "Test 
" + input.index + ": " + input.field);
-               }
-       }
+       // Note: toChronoUnit has been moved to GranularZonedDateTime as a 
private method.
+       // Tests for this functionality are now in GranularZonedDateTime_Test 
via the roll() method.
 
        
//-----------------------------------------------------------------------------------------------------------------
        // Round-trip conversion tests
@@ -297,191 +90,16 @@ class DateUtils_Test extends TestBase {
                        return INPUT;
                }
 
-               @ParameterizedTest
-               @MethodSource("input")
-               void g01_chronoFieldToChronoUnitToChronoField(Input input) {
-                       // ChronoField -> ChronoUnit -> ChronoField should be 
idempotent
-                       ChronoUnit unit = toChronoUnit(input.field);
-                       if (unit != null) {
-                               ChronoField result = toChronoField(unit);
-                               assertEquals(input.field, result, "Test " + 
input.index + ": " + input.field + " -> " + unit + " -> " + result);
-                       }
-               }
-
-               @ParameterizedTest
-               @MethodSource("input")
-               void g02_chronoFieldToCalendarField(Input input) {
-                       // ChronoField -> Calendar field should always work
-                       int calendarField = toCalendarField(input.field);
-                       assertTrue(calendarField >= 0, "Test " + input.index + 
": Calendar field should be non-negative");
-                       assertTrue(calendarField <= 18, "Test " + input.index + 
": Calendar field should be valid Calendar constant");
-               }
-       }
-
-       
//-----------------------------------------------------------------------------------------------------------------
-       // toIso8601(Calendar) tests
-       
//-----------------------------------------------------------------------------------------------------------------
-
-       static class H_toIso8601 {
-
-               private static final Input[] INPUT = {
-                       /* 01 */ input(1, 2024, JANUARY, 15, 14, 30, 45, 123, 
"UTC", "2024-01-15T14:30:45Z"),
-                       /* 02 */ input(2, 2024, JANUARY, 15, 14, 30, 45, 123, 
"America/New_York", "2024-01-15T14:30:45-05:00"),
-                       /* 03 */ input(3, 2024, JANUARY, 15, 14, 30, 45, 123, 
"America/Los_Angeles", "2024-01-15T14:30:45-08:00"),
-                       /* 04 */ input(4, 2024, JANUARY, 15, 14, 30, 45, 123, 
"Europe/London", "2024-01-15T14:30:45Z"),
-                       /* 05 */ input(5, 2024, JANUARY, 15, 14, 30, 45, 123, 
"Asia/Tokyo", "2024-01-15T14:30:45+09:00"),
-                       /* 06 */ input(6, 2024, JULY, 15, 14, 30, 45, 123, 
"America/New_York", "2024-07-15T14:30:45-04:00"), // DST
-                       /* 07 */ input(7, 2024, JULY, 15, 14, 30, 45, 123, 
"America/Los_Angeles", "2024-07-15T14:30:45-07:00"), // DST
-                       /* 08 */ input(8, 2024, JULY, 15, 14, 30, 45, 123, 
"Europe/London", "2024-07-15T14:30:45+01:00"), // DST
-                       /* 09 */ input(9, 2000, FEBRUARY, 29, 12, 0, 0, 0, 
"UTC", "2000-02-29T12:00:00Z"), // Leap year
-                       /* 10 */ input(10, 2024, DECEMBER, 31, 23, 59, 59, 999, 
"UTC", "2024-12-31T23:59:59Z"), // End of year
-                       /* 11 */ input(11, 2024, JANUARY, 1, 0, 0, 0, 0, "UTC", 
"2024-01-01T00:00:00Z"), // Start of year
-                       /* 12 */ input(12, 2024, JANUARY, 15, 0, 0, 0, 0, 
"UTC", "2024-01-15T00:00:00Z"), // Midnight
-                       /* 13 */ input(13, 2024, JANUARY, 15, 23, 59, 59, 999, 
"UTC", "2024-01-15T23:59:59Z"), // End of day
-                       /* 14 */ input(14, 2024, JANUARY, 15, 12, 0, 0, 0, 
"GMT+05:30", "2024-01-15T12:00:00+05:30"), // Custom offset
-                       /* 15 */ input(15, 2024, JANUARY, 15, 12, 0, 0, 0, 
"GMT-05:30", "2024-01-15T12:00:00-05:30") // Custom offset
-               };
-
-               private static Input input(int index, int year, int month, int 
day, int hour, int minute, int second, int millisecond, String timezone, String 
expectedIso8601) {
-                       return new Input(index, year, month, day, hour, minute, 
second, millisecond, timezone, expectedIso8601);
-               }
-
-               private static class Input {
-                       final int index;
-                       final int year;
-                       final int month;
-                       final int day;
-                       final int hour;
-                       final int minute;
-                       final int second;
-                       final int millisecond;
-                       final String timezone;
-                       final String expectedIso8601;
-
-                       public Input(int index, int year, int month, int day, 
int hour, int minute, int second, int millisecond, String timezone, String 
expectedIso8601) {
-                               this.index = index;
-                               this.year = year;
-                               this.month = month;
-                               this.day = day;
-                               this.hour = hour;
-                               this.minute = minute;
-                               this.second = second;
-                               this.millisecond = millisecond;
-                               this.timezone = timezone;
-                               this.expectedIso8601 = expectedIso8601;
-                       }
-               }
-
-               static Input[] input() {
-                       return INPUT;
-               }
-
-               @ParameterizedTest
-               @MethodSource("input")
-               void h01_toIso8601(Input input) {
-                       // Create Calendar with specified timezone and date/time
-                       var cal = 
Calendar.getInstance(TimeZone.getTimeZone(input.timezone));
-                       cal.set(input.year, input.month, input.day, input.hour, 
input.minute, input.second);
-                       cal.set(Calendar.MILLISECOND, input.millisecond);
-
-                       // Convert to ISO8601 string
-                       String result = toIso8601(cal);
-
-                       // Verify the result matches expected format
-                       assertEquals(input.expectedIso8601, result, "Test " + 
input.index + ": " + input.year + "-" + (input.month + 1) + "-" + input.day + " 
" + input.timezone);
-               }
-
-               @ParameterizedTest
-               @MethodSource("input")
-               void h02_toIso8601_formatValidation(Input input) {
-                       // Create Calendar with specified timezone and date/time
-                       var cal = 
Calendar.getInstance(TimeZone.getTimeZone(input.timezone));
-                       cal.set(input.year, input.month, input.day, input.hour, 
input.minute, input.second);
-                       cal.set(Calendar.MILLISECOND, input.millisecond);
-
-                       // Convert to ISO8601 string
-                       String result = toIso8601(cal);
-
-                       // Validate format structure
-                       
assertTrue(result.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"),
-                               "Test " + input.index + ": Result should match 
ISO8601 format: " + result);
-
-                       // Validate timezone format
-                       if (result.endsWith("Z")) {
-                               // UTC timezone
-                               assertTrue(result.endsWith("Z"), "Test " + 
input.index + ": UTC timezone should end with 'Z'");
-                       } else {
-                               // Offset timezone
-                               
assertTrue(result.matches(".*[+-]\\d{2}:\\d{2}$"), "Test " + input.index + ": 
Offset timezone should end with +/-HH:MM");
-                       }
-               }
        }
 
        
//-----------------------------------------------------------------------------------------------------------------
-       // toIso8601(Calendar) edge cases and error handling
+       // Helper method for converting Calendar to ISO8601 string
        
//-----------------------------------------------------------------------------------------------------------------
 
-       static class I_toIso8601_edgeCases {
-
-               @Test
-               void i01_nullCalendar() {
-                       assertThrows(NullPointerException.class, () -> {
-                               toIso8601(null);
-                       });
-               }
-
-               @Test
-               void i02_minimumDate() {
-                       var cal = 
Calendar.getInstance(TimeZone.getTimeZone("UTC"));
-                       cal.set(1, Calendar.JANUARY, 1, 0, 0, 0);
-                       cal.set(Calendar.MILLISECOND, 0);
-
-                       String result = toIso8601(cal);
-                       assertEquals("0001-01-01T00:00:00Z", result);
-               }
-
-               @Test
-               void i03_maximumDate() {
-                       var cal = 
Calendar.getInstance(TimeZone.getTimeZone("UTC"));
-                       cal.set(9999, Calendar.DECEMBER, 31, 23, 59, 59);
-                       cal.set(Calendar.MILLISECOND, 999);
-
-                       String result = toIso8601(cal);
-                       assertEquals("9999-12-31T23:59:59Z", result);
-               }
-
-               @Test
-               void i04_leapYear() {
-                       var cal = 
Calendar.getInstance(TimeZone.getTimeZone("UTC"));
-                       cal.set(2024, Calendar.FEBRUARY, 29, 12, 0, 0);
-                       cal.set(Calendar.MILLISECOND, 0);
-
-                       String result = toIso8601(cal);
-                       assertEquals("2024-02-29T12:00:00Z", result);
-               }
-
-               @Test
-               void i05_nonLeapYear() {
-                       var cal = 
Calendar.getInstance(TimeZone.getTimeZone("UTC"));
-                       cal.set(2023, Calendar.FEBRUARY, 28, 12, 0, 0);
-                       cal.set(Calendar.MILLISECOND, 0);
-
-                       String result = toIso8601(cal);
-                       assertEquals("2023-02-28T12:00:00Z", result);
-               }
-
-               @Test
-               void i06_dstTransition() {
-                       // Test DST transition in America/New_York (Spring 
forward)
-                       var cal = 
Calendar.getInstance(TimeZone.getTimeZone("America/New_York"));
-                       cal.set(2024, Calendar.MARCH, 10, 2, 30, 0); // 2:30 AM 
on DST transition day
-                       cal.set(Calendar.MILLISECOND, 0);
-
-                       String result = toIso8601(cal);
-                       // The exact result depends on how Java handles the DST 
transition
-                       assertTrue(result.contains("2024-03-10T"), "Should 
contain the date");
-                       assertTrue(result.contains(":30:00"), "Should contain 
the time");
-               }
+       private static String toIso8601(Calendar c) {
+               var sdf = new 
java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
+               sdf.setTimeZone(c.getTimeZone());
+               return sdf.format(c.getTime());
        }
 
        
//-----------------------------------------------------------------------------------------------------------------
@@ -564,21 +182,6 @@ class DateUtils_Test extends TestBase {
                        }
                }
 
-               @ParameterizedTest
-               @MethodSource("input")
-               void j02_fromIso8601Calendar_roundTrip(Input input) {
-                       // Parse the ISO8601 string
-                       Calendar cal = fromIso8601Calendar(input.iso8601String);
-                       assertNotNull(cal, "Test " + input.index + ": Calendar 
should not be null");
-
-                       // Convert back to ISO8601
-                       String result = toIso8601(cal);
-
-                       // The result should be a valid ISO8601 string
-                       assertNotNull(result, "Test " + input.index + ": Result 
should not be null");
-                       
assertTrue(result.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"),
-                               "Test " + input.index + ": Result should be 
valid ISO8601 format: " + result);
-               }
        }
 
        
//-----------------------------------------------------------------------------------------------------------------
@@ -776,79 +379,85 @@ class DateUtils_Test extends TestBase {
                }
        }
 
-       
//====================================================================================================
-       // addSubtractDays / add / toZonedDateTime
-       
//====================================================================================================
-       @Test
-       void test_addSubtractDays() {
-               var cal = Calendar.getInstance();
-               cal.set(2024, Calendar.JANUARY, 15, 0, 0, 0);
-               cal.set(Calendar.MILLISECOND, 0);
-
-               Calendar result = addSubtractDays(cal, 10);
-               assertNotNull(result);
-               assertNotSame(cal, result); // Should be a clone
-               assertEquals(25, result.get(Calendar.DAY_OF_MONTH));
-
-               result = addSubtractDays(cal, -5);
-               assertNotNull(result);
-               assertEquals(10, result.get(Calendar.DAY_OF_MONTH));
-
-               // Null calendar
-               assertNull(addSubtractDays(null, 10));
-       }
+       
//-----------------------------------------------------------------------------------------------------------------
+       // parseIsoCalendar(String) tests
+       
//-----------------------------------------------------------------------------------------------------------------
 
        @Test
-       void test_add() {
-               var cal = Calendar.getInstance();
-               cal.set(2024, Calendar.JANUARY, 15, 12, 30, 45);
-               cal.set(Calendar.MILLISECOND, 0);
-
-               // Add days
-               Calendar result = add(cal, Calendar.DAY_OF_MONTH, 5);
-               assertSame(cal, result); // Returns same instance
-               assertEquals(20, cal.get(Calendar.DAY_OF_MONTH));
-
-               // Add months
-               cal.set(2024, Calendar.JANUARY, 15, 0, 0, 0);
-               add(cal, Calendar.MONTH, 2);
-               assertEquals(Calendar.MARCH, cal.get(Calendar.MONTH));
-
-               // Add hours
-               cal.set(2024, Calendar.JANUARY, 15, 10, 0, 0);
-               add(cal, Calendar.HOUR_OF_DAY, 5);
-               assertEquals(15, cal.get(Calendar.HOUR_OF_DAY));
+       void m01_parseIsoCalendar() throws Exception {
+               // Various ISO8601 formats
+               var cal1 = parseIsoCalendar("2023");
+               assertNotNull(cal1);
+               assertEquals(2023, cal1.get(Calendar.YEAR));
+
+               var cal2 = parseIsoCalendar("2023-12");
+               assertNotNull(cal2);
+               assertEquals(2023, cal2.get(Calendar.YEAR));
+               assertEquals(Calendar.DECEMBER, cal2.get(Calendar.MONTH));
+
+               var cal3 = parseIsoCalendar("2023-12-25");
+               assertNotNull(cal3);
+               assertEquals(2023, cal3.get(Calendar.YEAR));
+               assertEquals(Calendar.DECEMBER, cal3.get(Calendar.MONTH));
+               assertEquals(25, cal3.get(Calendar.DAY_OF_MONTH));
+
+               var cal4 = parseIsoCalendar("2023-12-25T14:30:00");
+               assertNotNull(cal4);
+               assertEquals(14, cal4.get(Calendar.HOUR_OF_DAY));
+               assertEquals(30, cal4.get(Calendar.MINUTE));
+               assertEquals(0, cal4.get(Calendar.SECOND));
+
+               // Should throw for invalid dates (DateTimeParseException is 
thrown by DateUtils, not IllegalArgumentException)
+               assertThrows(Exception.class, () -> 
parseIsoCalendar("invalid"));
+               assertThrows(Exception.class, () -> 
parseIsoCalendar("2023-13-25")); // Invalid month
+
+               // Test empty input - triggers code path
+               assertNull(parseIsoCalendar(null));
+               assertNull(parseIsoCalendar(""));
+               assertNull(parseIsoCalendar("   "));
+
+               // Test with milliseconds (comma) - triggers code path
+               var cal5 = parseIsoCalendar("2023-12-25T14:30:00,123");
+               assertNotNull(cal5);
+               assertEquals(14, cal5.get(Calendar.HOUR_OF_DAY));
+               assertEquals(30, cal5.get(Calendar.MINUTE));
+               assertEquals(0, cal5.get(Calendar.SECOND)); // Milliseconds 
trimmed
+
+               // Test format yyyy-MM-ddThh - triggers code path
+               var cal6 = parseIsoCalendar("2023-12-25T14");
+               assertNotNull(cal6);
+               assertEquals(14, cal6.get(Calendar.HOUR_OF_DAY));
+               assertEquals(0, cal6.get(Calendar.MINUTE));
+               assertEquals(0, cal6.get(Calendar.SECOND));
+
+               // Test format yyyy-MM-ddThh:mm - triggers code path
+               var cal7 = parseIsoCalendar("2023-12-25T14:30");
+               assertNotNull(cal7);
+               assertEquals(14, cal7.get(Calendar.HOUR_OF_DAY));
+               assertEquals(30, cal7.get(Calendar.MINUTE));
+               assertEquals(0, cal7.get(Calendar.SECOND));
        }
 
-       @Test
-       void test_toZonedDateTime() {
-               var cal = new GregorianCalendar(2024, Calendar.JANUARY, 15, 12, 
30, 45);
-
-               Optional<ZonedDateTime> result = toZonedDateTime(cal);
-               assertTrue(result.isPresent());
-
-               ZonedDateTime zdt = result.get();
-               assertEquals(2024, zdt.getYear());
-               assertEquals(1, zdt.getMonthValue());
-               assertEquals(15, zdt.getDayOfMonth());
-               assertEquals(12, zdt.getHour());
-               assertEquals(30, zdt.getMinute());
-               assertEquals(45, zdt.getSecond());
-
-               // Null calendar
-               assertFalse(toZonedDateTime(null).isPresent());
-       }
+       
//-----------------------------------------------------------------------------------------------------------------
+       // parseIsoDate(String) tests
+       
//-----------------------------------------------------------------------------------------------------------------
 
        @Test
-       void test_toZonedDateTime_preservesTimezone() {
-               var tz = TimeZone.getTimeZone("America/New_York");
-               Calendar cal = new GregorianCalendar(tz);
-               cal.set(2024, Calendar.JANUARY, 15, 12, 30, 45);
+       void m02_parseIsoDate() throws Exception {
+               // parseIsoDate wraps parseIsoCalendar, so test similar cases
+               var date1 = parseIsoDate("2023-12-25");
+               assertNotNull(date1);
 
-               Optional<ZonedDateTime> result = toZonedDateTime(cal);
-               assertTrue(result.isPresent());
+               var date2 = parseIsoDate("2023-12-25T14:30:00");
+               assertNotNull(date2);
 
-               ZonedDateTime zdt = result.get();
-               assertEquals(tz.toZoneId(), zdt.getZone());
+               // Test empty input - triggers code path
+               // Note: parseIsoDate checks isEmpty before calling 
parseIsoCalendar, so it returns null
+               assertNull(parseIsoDate(null));
+               assertNull(parseIsoDate(""));
+
+               // Should throw for invalid dates (DateTimeParseException is 
thrown by DateUtils, not IllegalArgumentException)
+               assertThrows(Exception.class, () -> parseIsoDate("invalid"));
        }
+
 }
\ No newline at end of file
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/utils/StringUtils_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/utils/StringUtils_Test.java
index ac2ffe6c68..c6949cdc82 100755
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/utils/StringUtils_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/utils/StringUtils_Test.java
@@ -4410,84 +4410,6 @@ class StringUtils_Test extends TestBase {
                assertThrows(IllegalArgumentException.class, () -> 
parseIntWithSuffix(null));
        }
 
-       
//====================================================================================================
-       // parseIsoCalendar(String)
-       
//====================================================================================================
-       @Test
-       void a148_parseIsoCalendar() throws Exception {
-               // Various ISO8601 formats
-               var cal1 = parseIsoCalendar("2023");
-               assertNotNull(cal1);
-               assertEquals(2023, cal1.get(Calendar.YEAR));
-
-               var cal2 = parseIsoCalendar("2023-12");
-               assertNotNull(cal2);
-               assertEquals(2023, cal2.get(Calendar.YEAR));
-               assertEquals(Calendar.DECEMBER, cal2.get(Calendar.MONTH));
-
-               var cal3 = parseIsoCalendar("2023-12-25");
-               assertNotNull(cal3);
-               assertEquals(2023, cal3.get(Calendar.YEAR));
-               assertEquals(Calendar.DECEMBER, cal3.get(Calendar.MONTH));
-               assertEquals(25, cal3.get(Calendar.DAY_OF_MONTH));
-
-               var cal4 = parseIsoCalendar("2023-12-25T14:30:00");
-               assertNotNull(cal4);
-               assertEquals(14, cal4.get(Calendar.HOUR_OF_DAY));
-               assertEquals(30, cal4.get(Calendar.MINUTE));
-               assertEquals(0, cal4.get(Calendar.SECOND));
-
-               // Should throw for invalid dates (DateTimeParseException is 
thrown by DateUtils, not IllegalArgumentException)
-               assertThrows(Exception.class, () -> 
parseIsoCalendar("invalid"));
-               assertThrows(Exception.class, () -> 
parseIsoCalendar("2023-13-25")); // Invalid month
-
-               // Test empty input - triggers code path
-               assertNull(parseIsoCalendar(null));
-               assertNull(parseIsoCalendar(""));
-               assertNull(parseIsoCalendar("   "));
-
-               // Test with milliseconds (comma) - triggers code path
-               var cal5 = parseIsoCalendar("2023-12-25T14:30:00,123");
-               assertNotNull(cal5);
-               assertEquals(14, cal5.get(Calendar.HOUR_OF_DAY));
-               assertEquals(30, cal5.get(Calendar.MINUTE));
-               assertEquals(0, cal5.get(Calendar.SECOND)); // Milliseconds 
trimmed
-
-               // Test format yyyy-MM-ddThh - triggers code path
-               var cal6 = parseIsoCalendar("2023-12-25T14");
-               assertNotNull(cal6);
-               assertEquals(14, cal6.get(Calendar.HOUR_OF_DAY));
-               assertEquals(0, cal6.get(Calendar.MINUTE));
-               assertEquals(0, cal6.get(Calendar.SECOND));
-
-               // Test format yyyy-MM-ddThh:mm - triggers code path
-               var cal7 = parseIsoCalendar("2023-12-25T14:30");
-               assertNotNull(cal7);
-               assertEquals(14, cal7.get(Calendar.HOUR_OF_DAY));
-               assertEquals(30, cal7.get(Calendar.MINUTE));
-               assertEquals(0, cal7.get(Calendar.SECOND));
-       }
-
-       
//====================================================================================================
-       // parseIsoDate(String)
-       
//====================================================================================================
-       @Test
-       void a149_parseIsoDate() throws Exception {
-               // parseIsoDate wraps parseIsoCalendar, so test similar cases
-               var date1 = parseIsoDate("2023-12-25");
-               assertNotNull(date1);
-
-               var date2 = parseIsoDate("2023-12-25T14:30:00");
-               assertNotNull(date2);
-
-               // Test empty input - triggers code path
-               // Note: parseIsoDate checks isEmpty before calling 
parseIsoCalendar, so it returns null
-               assertNull(parseIsoDate(null));
-               assertNull(parseIsoDate(""));
-
-               // Should throw for invalid dates (DateTimeParseException is 
thrown by DateUtils, not IllegalArgumentException)
-               assertThrows(Exception.class, () -> parseIsoDate("invalid"));
-       }
 
        
//====================================================================================================
        // parseLong(String)
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java
index 9df3725530..ed66342b39 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java
@@ -176,14 +176,14 @@ class OpenApiPartSerializer_Test extends TestBase {
 
        @Test void c06_stringType_dateFormat() throws Exception {
                var ps = T_DATE;
-               var in = StringUtils.parseIsoCalendar("2012-12-21");
+               var in = DateUtils.parseIsoCalendar("2012-12-21");
                assertTrue(serialize(ps, in).contains("2012"));
                assertEquals("null", serialize(ps, null));
        }
 
        @Test void c07_stringType_dateTimeFormat() throws Exception {
                var ps = T_DATETIME;
-               var in = 
StringUtils.parseIsoCalendar("2012-12-21T12:34:56.789");
+               var in = DateUtils.parseIsoCalendar("2012-12-21T12:34:56.789");
                assertTrue(serialize(ps, in).contains("2012"));
                assertEquals("null", serialize(ps, null));
        }
@@ -918,13 +918,13 @@ class OpenApiPartSerializer_Test extends TestBase {
 
                assertEquals(
                        
"f01=foo,f02=Zm9v,f04=2012-12-21T12:34:56Z,f05=666F6F,f06=66 6F 
6F,f07=foo,f08=1,f09=2,f10=1.0,f11=1.0,f12=true,f99=1",
-                       serialize(ps, new 
H2("foo",foob,parseIsoCalendar("2012-12-21T12:34:56Z"),foob,foob,"foo",1,2,1.0,1.0,true,1))
+                       serialize(ps, new 
H2("foo",foob,DateUtils.parseIsoCalendar("2012-12-21T12:34:56Z"),foob,foob,"foo",1,2,1.0,1.0,true,1))
                );
                assertEquals("", serialize(ps, new 
H2(null,null,null,null,null,null,null,null,null,null,null,null)));
                assertEquals("null", serialize(ps, null));
                assertEquals(
                        
"f01=foo,f02=Zm9v,f04=2012-12-21T12:34:56Z,f05=666F6F,f06=66 6F 
6F,f07=foo,f08=1,f09=2,f10=1.0,f11=1.0,f12=true,f99=1",
-                       serialize(ps, 
JsonMap.of("f01","foo","f02",foob,"f04",parseIsoCalendar("2012-12-21T12:34:56Z"),"f05",foob,"f06",foob,"f07","foo","f08",1,"f09",2,"f10",1.0,"f11",1.0,"f12",true,"f99",1))
+                       serialize(ps, 
JsonMap.of("f01","foo","f02",foob,"f04",DateUtils.parseIsoCalendar("2012-12-21T12:34:56Z"),"f05",foob,"f06",foob,"f07","foo","f08",1,"f09",2,"f10",1.0,"f11",1.0,"f12",true,"f99",1))
                );
        }
 
@@ -948,7 +948,7 @@ class OpenApiPartSerializer_Test extends TestBase {
 
                assertEquals(
                        
"(f01=@('a,b',null),f02=@(Zm9v,null),f04=@(2012-12-21T12:34:56Z,null),f05=@(666F6F,null),f06=@('66
 6F 
6F',null),f07=@(a,b,null),f08=@(1,2,null),f09=@(3,4,null),f10=@(1.0,2.0,null),f11=@(3.0,4.0,null),f12=@(true,false,null),f99=@(1,x,null))",
-                       serialize(ps, new H2(a("a,b",null),new 
byte[][]{foob,null},a(parseIsoCalendar("2012-12-21T12:34:56Z"),null),new 
byte[][]{foob,null},new 
byte[][]{foob,null},a("a","b",null),a(1,2,null),a(3,4,null),a(1f,2f,null),a(3f,4f,null),a(true,false,null),a(1,"x",null)))
+                       serialize(ps, new H2(a("a,b",null),new 
byte[][]{foob,null},a(DateUtils.parseIsoCalendar("2012-12-21T12:34:56Z"),null),new
 byte[][]{foob,null},new 
byte[][]{foob,null},a("a","b",null),a(1,2,null),a(3,4,null),a(1f,2f,null),a(3f,4f,null),a(true,false,null),a(1,"x",null)))
                );
 
        }

Reply via email to