Skip to content

Commit

Permalink
- All older Java date-time classes and newer temporal classes, output…
Browse files Browse the repository at this point in the history
… in "one field" when in Map form.

- Many, many new tests added
- Timezone handling improved - ZonedDateTime ISO_ZONE_DATE_TIME format used consistently and round trips.
- GMT time supported but turns into Etc/GMT internally.
  • Loading branch information
jdereg committed Feb 3, 2025
1 parent c6999fe commit 157df54
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 192 deletions.
14 changes: 8 additions & 6 deletions src/main/java/com/cedarsoftware/util/ClassUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -766,17 +766,19 @@ public static <T> T findClosest(Class<?> clazz, Map<Class<?>, T> candidateClasse
Objects.requireNonNull(clazz, "Class cannot be null");
Objects.requireNonNull(candidateClasses, "CandidateClasses classes map cannot be null");

// First try exact match
T exactMatch = candidateClasses.get(clazz);
if (exactMatch != null) {
return exactMatch;
}

// If no exact match, then look for closest inheritance match
T closest = defaultClass;
int minDistance = Integer.MAX_VALUE;
Class<?> closestClass = null; // Track the actual class for tie-breaking
Class<?> closestClass = null;

for (Map.Entry<Class<?>, T> entry : candidateClasses.entrySet()) {
Class<?> candidateClass = entry.getKey();
// Direct match - return immediately
if (candidateClass == clazz) {
return entry.getValue();
}

int distance = ClassUtilities.computeInheritanceDistance(clazz, candidateClass);
if (distance != -1 && (distance < minDistance ||
(distance == minDistance && shouldPreferNewCandidate(candidateClass, closestClass)))) {
Expand Down
15 changes: 10 additions & 5 deletions src/main/java/com/cedarsoftware/util/DateUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@
* limitations under the License.
*/
public final class DateUtilities {
private static final Pattern allDigits = Pattern.compile("^\\d+$");
private static final Pattern allDigits = Pattern.compile("^-?\\d+$");
private static final String days = "monday|mon|tuesday|tues|tue|wednesday|wed|thursday|thur|thu|friday|fri|saturday|sat|sunday|sun"; // longer before shorter matters
private static final String mos = "January|Jan|February|Feb|March|Mar|April|Apr|May|June|Jun|July|Jul|August|Aug|September|Sept|Sep|October|Oct|November|Nov|December|Dec";
private static final String yr = "[+-]?\\d{4,5}\\b";
private static final String yr = "[+-]?\\d{4,9}\\b";
private static final String d1or2 = "\\d{1,2}";
private static final String d2 = "\\d{2}";
private static final String ord = "st|nd|rd|th";
Expand Down Expand Up @@ -556,18 +556,23 @@ private static ZoneId getTimeZone(String tz) {
return ZoneId.ofOffset("GMT", offset);
}

// 2) Check custom abbreviation map first
// 2) Handle GMT explicitly to normalize to Etc/GMT
if (tz.equals("GMT")) {
return ZoneId.of("Etc/GMT");
}

// 3) Check custom abbreviation map first
String mappedZone = ABBREVIATION_TO_TIMEZONE.get(tz.toUpperCase());
if (mappedZone != null) {
// e.g. "EST" => "America/New_York"
return ZoneId.of(mappedZone);
}

// 3) Try ZoneId.of(tz) for full region IDs like "Europe/Paris"
// 4) Try ZoneId.of(tz) for full region IDs like "Europe/Paris"
try {
return ZoneId.of(tz);
} catch (Exception zoneIdEx) {
// 4) Fallback to TimeZone for weird short IDs or older JDK
// 5) Fallback to TimeZone for weird short IDs or older JDK
TimeZone timeZone = TimeZone.getTimeZone(tz);
if (timeZone.getID().equals("GMT") && !tz.toUpperCase().equals("GMT")) {
// Means the JDK didn't recognize 'tz' (it fell back to "GMT")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicLong;

import com.cedarsoftware.util.CompactMap;
import static com.cedarsoftware.util.convert.MapConversions.INSTANT;

/**
* @author Kenny Partlow ([email protected])
Expand All @@ -38,13 +39,11 @@
final class InstantConversions {

private InstantConversions() {}

static Map toMap(Object from, Converter converter) {
long sec = ((Instant) from).getEpochSecond();
int nanos = ((Instant) from).getNano();
Map<String, Object> target = CompactMap.<String, Object>builder().insertionOrder().build();
target.put("seconds", sec);
target.put("nanos", nanos);
Instant instant = (Instant) from;
Map<String, Object> target = new LinkedHashMap<>();
target.put(INSTANT, instant.toString()); // Uses ISO-8601 format
return target;
}

Expand Down
170 changes: 101 additions & 69 deletions src/main/java/com/cedarsoftware/util/convert/MapConversions.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,13 @@ final class MapConversions {
static final String DATE = "date";
static final String SQL_DATE = "sqlDate";
static final String CALENDAR = "calendar";
static final String TIME = "time";
static final String TIMESTAMP = "timestamp";
static final String DURATION = "duration";
static final String INSTANT = "instant";
static final String MONTH_DAY = "monthDay";
static final String YEAR_MONTH = "yearMonth";
static final String PERIOD = "period";
static final String ZONE_OFFSET = "zoneOffset";
static final String LOCAL_DATE = "localDate";
static final String LOCAL_TIME = "localTime";
static final String LOCAL_DATE_TIME = "localDateTime";
Expand All @@ -75,24 +79,13 @@ final class MapConversions {
static final String ZONED_DATE_TIME = "zonedDateTime";
static final String ZONE = "zone";
static final String YEAR = "year";
static final String YEARS = "years";
static final String MONTH = "month";
static final String MONTHS = "months";
static final String DAY = "day";
static final String DAYS = "days";
static final String HOUR = "hour";
static final String HOURS = "hours";
static final String MINUTE = "minute";
static final String MINUTES = "minutes";
static final String SECOND = "second";
static final String SECONDS = "seconds";
static final String EPOCH_MILLIS = "epochMillis";
static final String NANOS = "nanos";
static final String MOST_SIG_BITS = "mostSigBits";
static final String LEAST_SIG_BITS = "leastSigBits";
static final String OFFSET = "offset";
static final String OFFSET_HOUR = "offsetHour";
static final String OFFSET_MINUTE = "offsetMinute";
static final String ID = "id";
static final String LANGUAGE = "language";
static final String COUNTRY = "country";
Expand Down Expand Up @@ -395,61 +388,59 @@ static LocalTime toLocalTime(Object from, Converter converter) {
return fromMap(from, converter, LocalTime.class, new String[]{LOCAL_TIME});
}

private static final String[] OFFSET_TIME_KEYS = {OFFSET_TIME, VALUE, V};
private static final String[] LDT_KEYS = {LOCAL_DATE_TIME, VALUE, V, EPOCH_MILLIS};

static OffsetTime toOffsetTime(Object from, Converter converter) {
static LocalDateTime toLocalDateTime(Object from, Converter converter) {
Map<String, Object> map = (Map<String, Object>) from;
Object value = null;
for (String key : OFFSET_TIME_KEYS) {
for (String key : LDT_KEYS) {
value = map.get(key);
if (value != null && (!(value instanceof String) || StringUtilities.hasContent((String) value))) {
break;
}
}

// If the 'offsetDateTime' value is a non-empty String, parse it (allows for value, _v, epochMillis as String)
if (value instanceof String && StringUtilities.hasContent((String) value)) {
return StringConversions.toOffsetTime(value, converter);
return StringConversions.toLocalDateTime(value, converter);
}

if (value instanceof Number) {
return NumberConversions.toOffsetTime(value, converter);
return NumberConversions.toLocalDateTime(value, converter);
}

return fromMap(from, converter, OffsetTime.class, new String[]{OFFSET_TIME});
return fromMap(from, converter, LocalDateTime.class, new String[] {LOCAL_DATE_TIME}, new String[] {EPOCH_MILLIS});
}

private static final String[] OFFSET_KEYS = {OFFSET_DATE_TIME, VALUE, V, EPOCH_MILLIS};
private static final String[] OFFSET_TIME_KEYS = {OFFSET_TIME, VALUE, V};

static OffsetDateTime toOffsetDateTime(Object from, Converter converter) {
static OffsetTime toOffsetTime(Object from, Converter converter) {
Map<String, Object> map = (Map<String, Object>) from;
Object value = null;
for (String key : OFFSET_KEYS) {
for (String key : OFFSET_TIME_KEYS) {
value = map.get(key);
if (value != null && (!(value instanceof String) || StringUtilities.hasContent((String) value))) {
break;
}
}

// If the 'offsetDateTime' value is a non-empty String, parse it (allows for value, _v, epochMillis as String)
if (value instanceof String && StringUtilities.hasContent((String) value)) {
return StringConversions.toOffsetDateTime(value, converter);
return StringConversions.toOffsetTime(value, converter);
}

// Otherwise, if epoch_millis is provided, use it with the nanos (if any)
if (value instanceof Number) {
long ms = converter.convert(value, long.class);
return NumberConversions.toOffsetDateTime(ms, converter);
return NumberConversions.toOffsetTime(value, converter);
}
return fromMap(from, converter, OffsetDateTime.class, new String[] {OFFSET_DATE_TIME}, new String[] {EPOCH_MILLIS});

return fromMap(from, converter, OffsetTime.class, new String[]{OFFSET_TIME});
}

private static final String[] LDT_KEYS = {LOCAL_DATE_TIME, VALUE, V, EPOCH_MILLIS};
private static final String[] OFFSET_KEYS = {OFFSET_DATE_TIME, VALUE, V, EPOCH_MILLIS};

static LocalDateTime toLocalDateTime(Object from, Converter converter) {
static OffsetDateTime toOffsetDateTime(Object from, Converter converter) {
Map<String, Object> map = (Map<String, Object>) from;
Object value = null;
for (String key : LDT_KEYS) {
for (String key : OFFSET_KEYS) {
value = map.get(key);
if (value != null && (!(value instanceof String) || StringUtilities.hasContent((String) value))) {
break;
Expand All @@ -458,14 +449,16 @@ static LocalDateTime toLocalDateTime(Object from, Converter converter) {

// If the 'offsetDateTime' value is a non-empty String, parse it (allows for value, _v, epochMillis as String)
if (value instanceof String && StringUtilities.hasContent((String) value)) {
return StringConversions.toLocalDateTime(value, converter);
return StringConversions.toOffsetDateTime(value, converter);
}

// Otherwise, if epoch_millis is provided, use it with the nanos (if any)
if (value instanceof Number) {
return NumberConversions.toLocalDateTime(value, converter);
long ms = converter.convert(value, long.class);
return NumberConversions.toOffsetDateTime(ms, converter);
}

return fromMap(from, converter, LocalDateTime.class, new String[] {LOCAL_DATE_TIME}, new String[] {EPOCH_MILLIS});
return fromMap(from, converter, OffsetDateTime.class, new String[] {OFFSET_DATE_TIME}, new String[] {EPOCH_MILLIS});
}

private static final String[] ZDT_KEYS = {ZONED_DATE_TIME, VALUE, V, EPOCH_MILLIS};
Expand Down Expand Up @@ -522,54 +515,84 @@ static Duration toDuration(Object from, Converter converter) {
return fromMap(from, converter, Duration.class, new String[] {SECONDS, NANOS + OPTIONAL});
}

private static final String[] INSTANT_KEYS = {INSTANT, VALUE, V};

static Instant toInstant(Object from, Converter converter) {
Map<String, Object> map = (Map<String, Object>) from;
Object seconds = map.get(SECONDS);
if (seconds != null) {
long sec = converter.convert(seconds, long.class);
long nanos = converter.convert(map.get(NANOS), long.class);
return Instant.ofEpochSecond(sec, nanos);
Object value = null;
for (String key : INSTANT_KEYS) {
value = map.get(key);
if (value != null && (!(value instanceof String) || StringUtilities.hasContent((String) value))) {
break;
}
}
return fromMap(from, converter, Instant.class, new String[] {SECONDS, NANOS + OPTIONAL});

// If the 'instant' value is a non-empty String, parse it (allows for value, _v, epochMillis as String)
if (value instanceof String && StringUtilities.hasContent((String) value)) {
return StringConversions.toInstant(value, converter);
}

return fromMap(from, converter, Instant.class, new String[] {INSTANT});
}

private static final String[] MONTH_DAY_KEYS = {MONTH_DAY, VALUE, V};

static MonthDay toMonthDay(Object from, Converter converter) {
Map<String, Object> map = (Map<String, Object>) from;
Object month = map.get(MONTH);
Object day = map.get(DAY);
if (month != null && day != null) {
int m = converter.convert(month, int.class);
int d = converter.convert(day, int.class);
return MonthDay.of(m, d);
Object value = null;
for (String key : MONTH_DAY_KEYS) {
value = map.get(key);
if (value != null && (!(value instanceof String) || StringUtilities.hasContent((String) value))) {
break;
}
}
return fromMap(from, converter, MonthDay.class, new String[] {MONTH, DAY});

// If the 'monthDay' value is a non-empty String, parse it (allows for value, _v, epochMillis as String)
if (value instanceof String && StringUtilities.hasContent((String) value)) {
return StringConversions.toMonthDay(value, converter);
}

return fromMap(from, converter, MonthDay.class, new String[] {MONTH_DAY});
}

private static final String[] YEAR_MONTH_KEYS = {YEAR_MONTH, VALUE, V};

static YearMonth toYearMonth(Object from, Converter converter) {
Map<String, Object> map = (Map<String, Object>) from;
Object year = map.get(YEAR);
Object month = map.get(MONTH);
if (year != null && month != null) {
int y = converter.convert(year, int.class);
int m = converter.convert(month, int.class);
return YearMonth.of(y, m);
Object value = null;
for (String key : YEAR_MONTH_KEYS) {
value = map.get(key);
if (value != null && (!(value instanceof String) || StringUtilities.hasContent((String) value))) {
break;
}
}
return fromMap(from, converter, YearMonth.class, new String[] {YEAR, MONTH});

// If the 'yearMonth' value is a non-empty String, parse it (allows for value, _v, epochMillis as String)
if (value instanceof String && StringUtilities.hasContent((String) value)) {
return StringConversions.toYearMonth(value, converter);
}

return fromMap(from, converter, YearMonth.class, new String[] {YEAR_MONTH});
}

static Period toPeriod(Object from, Converter converter) {
private static final String[] PERIOD_KEYS = {PERIOD, VALUE, V};

static Period toPeriod(Object from, Converter converter) {
Map<String, Object> map = (Map<String, Object>) from;

if (map.containsKey(VALUE) || map.containsKey(V)) {
return fromMap(from, converter, Period.class, new String[] {YEARS, MONTHS, DAYS});
Object value = null;
for (String key : PERIOD_KEYS) {
value = map.get(key);
if (value != null && (!(value instanceof String) || StringUtilities.hasContent((String) value))) {
break;
}
}

Number years = converter.convert(map.getOrDefault(YEARS, 0), int.class);
Number months = converter.convert(map.getOrDefault(MONTHS, 0), int.class);
Number days = converter.convert(map.getOrDefault(DAYS, 0), int.class);
// If the 'zonedDateTime' value is a non-empty String, parse it (allows for value, _v, epochMillis as String)
if (value instanceof String && StringUtilities.hasContent((String) value)) {
return StringConversions.toPeriod(value, converter);
}

return Period.of(years.intValue(), months.intValue(), days.intValue());
return fromMap(from, converter, Period.class, new String[] {PERIOD});
}

static ZoneId toZoneId(Object from, Converter converter) {
Expand All @@ -585,15 +608,24 @@ static ZoneId toZoneId(Object from, Converter converter) {
return fromMap(from, converter, ZoneId.class, new String[] {ZONE}, new String[] {ID});
}

private static final String[] ZONE_OFFSET_KEYS = {ZONE_OFFSET, VALUE, V};

static ZoneOffset toZoneOffset(Object from, Converter converter) {
Map<String, Object> map = (Map<String, Object>) from;
if (map.containsKey(HOURS)) {
int hours = converter.convert(map.get(HOURS), int.class);
int minutes = converter.convert(map.getOrDefault(MINUTES, 0), int.class); // optional
int seconds = converter.convert(map.getOrDefault(SECONDS, 0), int.class); // optional
return ZoneOffset.ofHoursMinutesSeconds(hours, minutes, seconds);
Object value = null;
for (String key : ZONE_OFFSET_KEYS) {
value = map.get(key);
if (value != null && (!(value instanceof String) || StringUtilities.hasContent((String) value))) {
break;
}
}

// If the 'zonedDateTime' value is a non-empty String, parse it (allows for value, _v, epochMillis as String)
if (value instanceof String && StringUtilities.hasContent((String) value)) {
return StringConversions.toZoneOffset(value, converter);
}
return fromMap(from, converter, ZoneOffset.class, new String[] {HOURS, MINUTES + OPTIONAL, SECONDS + OPTIONAL});

return fromMap(from, converter, ZoneOffset.class, new String[] {ZONE_OFFSET});
}

static Year toYear(Object from, Converter converter) {
Expand Down
Loading

0 comments on commit 157df54

Please sign in to comment.