Skip to content

Commit

Permalink
Full Calendar to Map and Map to Calendar support added.
Browse files Browse the repository at this point in the history
  • Loading branch information
jdereg committed Mar 3, 2024
1 parent a4150a1 commit 3c49663
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/main/java/com/cedarsoftware/util/Converter.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
* to all destinations per each source. Close to 500 "out-of-the-box" conversions ship with the library.<br>
* <br>
* The Converter can be used as statically or as an instance. See the public static methods on this Converter class
* to use statically. Any added conversions will added to a singleton instance maintained inside this class.
* to use statically. Any added conversions are added to a singleton instance maintained inside this class.
* Alternatively, you can instantiate the Converter class to get an instance, and the conversions you add, remove, or
* change will be scoped to just that instance. <br>
* <br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import com.cedarsoftware.util.CompactLinkedMap;

/**
* @author Kenny Partlow ([email protected])
* <br>
Expand Down Expand Up @@ -106,8 +109,22 @@ static Calendar create(long epochMilli, Converter converter) {
}

static String toString(Object from, Converter converter) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
simpleDateFormat.setTimeZone(converter.getOptions().getTimeZone());
return simpleDateFormat.format(((Calendar) from).getTime());
ZonedDateTime zdt = toZonedDateTime(from, converter);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX", converter.getOptions().getLocale());
return formatter.format(zdt);
}

static Map<String, Object> toMap(Object from, Converter converter) {
Calendar cal = (Calendar) from;
Map<String, Object> target = new CompactLinkedMap<>();
target.put(MapConversions.YEAR, cal.get(Calendar.YEAR));
target.put(MapConversions.MONTH, cal.get(Calendar.MONTH) + 1);
target.put(MapConversions.DAY, cal.get(Calendar.DAY_OF_MONTH));
target.put(MapConversions.HOUR, cal.get(Calendar.HOUR_OF_DAY));
target.put(MapConversions.MINUTE, cal.get(Calendar.MINUTE));
target.put(MapConversions.SECOND, cal.get(Calendar.SECOND));
target.put(MapConversions.MILLI_SECONDS, cal.get(Calendar.MILLISECOND));
target.put(MapConversions.ZONE, cal.getTimeZone().getID());
return target;
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/cedarsoftware/util/convert/Converter.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
* Boolean, for example, however, for primitive types, it chooses zero for the numeric ones, `false` for boolean,
* and 0 for char.<br>
* <br>
* A Map can be converted to almost all JDL "data" classes. For example, UUID can be converted to/from a Map.
* A Map can be converted to almost all JDK "data" classes. For example, UUID can be converted to/from a Map.
* It is expected for the Map to have certain keys ("mostSigBits", "leastSigBits"). For the older Java Date/Time
* related classes, it expects "time" or "nanos", and for all others, a Map as the source, the "value" key will be
* used to source the value for the conversion.<br>
Expand Down Expand Up @@ -904,7 +904,7 @@ private static void buildFactoryConversions() {
CONVERSION_DB.put(pair(ZoneOffset.class, Map.class), ZoneOffsetConversions::toMap);
CONVERSION_DB.put(pair(Class.class, Map.class), MapConversions::initMap);
CONVERSION_DB.put(pair(UUID.class, Map.class), MapConversions::initMap);
CONVERSION_DB.put(pair(Calendar.class, Map.class), MapConversions::initMap);
CONVERSION_DB.put(pair(Calendar.class, Map.class), CalendarConversions::toMap);
CONVERSION_DB.put(pair(Number.class, Map.class), MapConversions::initMap);
CONVERSION_DB.put(pair(Map.class, Map.class), MapConversions::toMap);
CONVERSION_DB.put(pair(Enum.class, Map.class), MapConversions::initMap);
Expand Down
37 changes: 33 additions & 4 deletions src/main/java/com/cedarsoftware/util/convert/MapConversions.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ final class MapConversions {
static final String MINUTES = "minutes";
static final String SECOND = "second";
static final String SECONDS = "seconds";
static final String MILLI_SECONDS = "millis";
static final String NANO = "nano";
static final String NANOS = "nanos";
static final String OFFSET_HOUR = "offsetHour";
Expand Down Expand Up @@ -205,24 +206,52 @@ static TimeZone toTimeZone(Object from, Converter converter) {
}
}

private static final String[] CALENDAR_PARAMS = new String[] { TIME, ZONE };
private static final String[] CALENDAR_PARAMS = new String[] { YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, MILLI_SECONDS, ZONE };
static Calendar toCalendar(Object from, Converter converter) {
Map<?, ?> map = (Map<?, ?>) from;
if (map.containsKey(TIME)) {
Object zoneRaw = map.get(ZONE);
TimeZone tz;
ConverterOptions options = converter.getOptions();

if (zoneRaw instanceof String) {
String zone = (String) zoneRaw;
tz = TimeZone.getTimeZone(zone);
} else {
tz = TimeZone.getTimeZone(options.getZoneId());
tz = TimeZone.getTimeZone(converter.getOptions().getZoneId());
}

Calendar cal = Calendar.getInstance(tz);
Date epochInMillis = converter.convert(map.get(TIME), Date.class);
cal.setTimeInMillis(epochInMillis.getTime());
return cal;
}
else if (map.containsKey(YEAR)) {
int year = converter.convert(map.get(YEAR), int.class);
int month = converter.convert(map.get(MONTH), int.class);
int day = converter.convert(map.get(DAY), int.class);
int hour = converter.convert(map.get(HOUR), int.class);
int minute = converter.convert(map.get(MINUTE), int.class);
int second = converter.convert(map.get(SECOND), int.class);
int ms = converter.convert(map.get(MILLI_SECONDS), int.class);
Object zoneRaw = map.get(ZONE);

TimeZone tz;

if (zoneRaw instanceof String) {
String zone = (String) zoneRaw;
tz = TimeZone.getTimeZone(zone);
} else {
tz = TimeZone.getTimeZone(converter.getOptions().getZoneId());
}

Calendar cal = Calendar.getInstance(tz);
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, month - 1);
cal.set(Calendar.DAY_OF_MONTH, day);
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, second);
cal.set(Calendar.MILLISECOND, ms);
cal.getTime();
return cal;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ private PeriodConversions() {}
static Map toMap(Object from, Converter converter) {
Period period = (Period) from;
Map<String, Object> target = new CompactLinkedMap<>();
target.put("years", period.getYears());
target.put("months", period.getMonths());
target.put("days", period.getDays());
target.put(MapConversions.YEARS, period.getYears());
target.put(MapConversions.MONTHS, period.getMonths());
target.put(MapConversions.DAYS, period.getDays());
return target;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import java.time.format.DateTimeParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.Optional;
import java.util.TimeZone;
Expand Down Expand Up @@ -362,7 +361,29 @@ static TimeZone toTimeZone(Object from, Converter converter) {
}

static Calendar toCalendar(Object from, Converter converter) {
return parseDate(from, converter).map(GregorianCalendar::from).orElse(null);
String calStr = (String) from;
if (StringUtilities.isEmpty(calStr)) {
return null;
}
ZonedDateTime zdt = DateUtilities.parseDate(calStr, converter.getOptions().getZoneId(), true);
if (zdt == null) {
return null;
}
ZonedDateTime zdtUser = zdt.withZoneSameInstant(converter.getOptions().getZoneId());

// Must copy this way. Using the GregorianCalendar.from(zdt) does not pass .equals() later.
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, zdt.getYear());
cal.set(Calendar.MONTH, zdt.getMonthValue() - 1);
cal.set(Calendar.DAY_OF_MONTH, zdt.getDayOfMonth());
cal.set(Calendar.HOUR_OF_DAY, zdt.getHour());
cal.set(Calendar.MINUTE, zdt.getMinute());
cal.set(Calendar.SECOND, zdt.getSecond());
cal.set(Calendar.MILLISECOND, zdt.getNano() / 1_000_000);
cal.setTimeZone(TimeZone.getTimeZone(zdtUser.getZone()));
cal.getTime();

return cal;
}

static LocalDate toLocalDate(Object from, Converter converter) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.stream.Stream;

import com.cedarsoftware.util.ClassUtilities;
import com.cedarsoftware.util.CompactLinkedMap;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -184,6 +185,26 @@ private static void loadMapTests() {
{BigDecimal.valueOf(1), mapOf(VALUE, BigDecimal.valueOf(1))},
{BigDecimal.valueOf(2), mapOf(VALUE, BigDecimal.valueOf(2))}
});
TEST_DB.put(pair(Calendar.class, Map.class), new Object[][]{
{(Supplier<Calendar>) () -> {
Calendar cal = Calendar.getInstance(TOKYO_TZ);
cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 17);
cal.set(Calendar.MILLISECOND, 409);
cal.getTime();
return cal;
}, (Supplier<Map<String, Object>>) () -> {
Map<String, Object> map = new CompactLinkedMap<>();
map.put(MapConversions.YEAR, 2024);
map.put(MapConversions.MONTH, 2);
map.put(MapConversions.DAY, 5);
map.put(MapConversions.HOUR, 22);
map.put(MapConversions.MINUTE, 31);
map.put(MapConversions.SECOND, 17);
map.put(MapConversions.MILLI_SECONDS, 409);
map.put(MapConversions.ZONE, TOKYO);
return map;
}, true},
});
}

/**
Expand Down Expand Up @@ -398,10 +419,11 @@ private static void loadStringTests() {
TEST_DB.put(pair(Calendar.class, String.class), new Object[][]{
{(Supplier<Calendar>) () -> {
Calendar cal = Calendar.getInstance(TOKYO_TZ);
cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 0);
cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 17);
cal.set(Calendar.MILLISECOND, 409);
cal.getTime();
return cal;
}, "2024-02-05T22:31:00"}
}, "2024-02-05T22:31:17.409+09:00"}
});
TEST_DB.put(pair(Number.class, String.class), new Object[][]{
{(byte) 1, "1"},
Expand Down Expand Up @@ -1159,16 +1181,36 @@ private static void loadCalendarTests() {
{new BigDecimal("0.001"), (Supplier<Calendar>) () -> {
Calendar cal = Calendar.getInstance(TOKYO_TZ);
cal.setTimeInMillis(1);
cal.getTime();
return cal;
}, true},
{new BigDecimal(1), (Supplier<Calendar>) () -> {
Calendar cal = Calendar.getInstance(TOKYO_TZ);
cal.setTimeInMillis(1000);
cal.getTime();
return cal;
}, true},
});
TEST_DB.put(pair(Map.class, Calendar.class), new Object[][]{
{(Supplier<Map<String, Object>>) () -> {
Map<String, Object> map = new CompactLinkedMap<>();
map.put(VALUE, "2024-02-05T22:31:17.409[" + TOKYO + "]");
return map;
}, (Supplier<Calendar>) () -> {
Calendar cal = Calendar.getInstance(TOKYO_TZ);
cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 17);
cal.set(Calendar.MILLISECOND, 409);
return cal;
}},
{(Supplier<Map<String, Object>>) () -> {
Map<String, Object> map = new CompactLinkedMap<>();
map.put(VALUE, "2024-02-05T22:31:17.409" + TOKYO_ZO.toString());
return map;
}, (Supplier<Calendar>) () -> {
Calendar cal = Calendar.getInstance(TOKYO_TZ);
cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 17);
cal.set(Calendar.MILLISECOND, 409);
return cal;
}},
});
}

/**
Expand Down Expand Up @@ -3023,7 +3065,14 @@ void testConvert(String shortNameSource, String shortNameTarget, Object source,
}
}
catch (Throwable e) {
System.err.println(shortNameSource + "[" + source + "] ==> " + shortNameTarget + "[" + target + "] Failed with: " + actual);
String actualClass;
if (actual == null) {
actualClass = "Class:null";
} else {
actualClass = Converter.getShortName(actual.getClass());
}

System.err.println(shortNameSource + "[" + toStr(source) + "] ==> " + shortNameTarget + "[" + toStr(target) + "] Failed with: " + actualClass + "[" + toStr(actual) + "]");
throw e;
}
}
Expand All @@ -3033,6 +3082,15 @@ private static void updateStat(Map.Entry<Class<?>, Class<?>> pair, boolean state
STAT_DB.put(pair, state);
}

private String toStr(Object o) {
if (o instanceof Calendar) {
Calendar cal = (Calendar) o;
return CalendarConversions.toString(cal, converter);
} else {
return o.toString();
}
}

// Rare pairings that cannot be tested without drilling into the class - Atomic's require .get() to be called,
// so an Atomic inside a Map is a hard-case.
private static boolean isHardCase(Class<?> sourceClass, Class<?> targetClass) {
Expand Down
15 changes: 7 additions & 8 deletions src/test/java/com/cedarsoftware/util/convert/ConverterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1800,11 +1800,12 @@ void testString_fromDate()
@Test
void testString_fromCalendar()
{
Calendar cal = Calendar.getInstance();
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.clear();
cal.set(2015, 0, 17, 8, 34, 49);
assertEquals("2015-01-17T08:34:49", this.converter.convert(cal.getTime(), String.class));
assertEquals("2015-01-17T08:34:49", this.converter.convert(cal, String.class));
// TODO: Gets fixed when Date.class ==> String.class is tested/added
// assertEquals("2015-01-17T08:34:49", this.converter.convert(cal.getTime(), String.class));
assertEquals("2015-01-17T08:34:49.000Z", this.converter.convert(cal, String.class));
}

@Test
Expand Down Expand Up @@ -2851,7 +2852,7 @@ void testMapToCalendar(Object value)
map.clear();
assertThatThrownBy(() -> this.converter.convert(map, Calendar.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("To convert from Map to Calendar the map must include one of the following: [time, zone], [_v], or [value] with associated values");
.hasMessageContaining("Map to Calendar the map must include one of the following: [year, month, day, hour, minute, second, millis, zone], [_v], or [value]");
}

@Test
Expand Down Expand Up @@ -2909,7 +2910,7 @@ void testMapToGregCalendar()
map.clear();
assertThatThrownBy(() -> this.converter.convert(map, GregorianCalendar.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("To convert from Map to Calendar the map must include one of the following: [time, zone], [_v], or [value] with associated values");
.hasMessageContaining("Map to Calendar the map must include one of the following: [year, month, day, hour, minute, second, millis, zone], [_v], or [value]");
}

@Test
Expand Down Expand Up @@ -3702,9 +3703,7 @@ void testCalendarToMap()
{
Calendar cal = Calendar.getInstance();
Map<?, ?> map = this.converter.convert(cal, Map.class);
assert map.size() == 1;
assertEquals(map.get(VALUE), cal);
assert map.get(VALUE) instanceof Calendar;
assert map.size() == 8;
}

@Test
Expand Down

0 comments on commit 3c49663

Please sign in to comment.