Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to date parsing out-of-the-box #124

Merged
merged 8 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,91 @@

package net.obvj.confectory.util;

import java.sql.Timestamp;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.TemporalAccessor;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import org.apache.commons.lang3.ClassUtils;

/**
* A class that contains built-in parsers from string into common object types, typically
* for Reflection purposes.
* <p>
* It supports all of the primitive types (including their wrappers) as well as the
* following object types:
* <p>
* <b><i>Since 2.5.0:</i></b>
* <ul>
* <li>{@code java.sql.Date} such as {@code "2007-12-03"}</li>
* <li>{@code java.sql.Timestamp} such as {@code "2007-12-03 10:15:30.998"}</li>
* <li>{@code java.time.DayOfWeek} such as {@code "FRIDAY"}</li>
* <li>{@code java.time.Duration} such as {@code "PT15M"} (15 minutes)</li>
* <li>{@code java.time.Instant} such as {@code "2007-12-03T13:15:30Z"}</li>
* <li>{@code java.time.LocalDate} such as {@code "2007-12-03"}</li>
* <li>{@code java.time.LocalDateTime} such as {@code "2007-12-03T10:15:30"}</li>
* <li>{@code java.time.Month} such as {@code "DECEMBER"}</li>
* <li>{@code java.time.OffsetDateTime} such as {@code "2007-12-03T10:15:30-03:00"}</li>
* <li>{@code java.time.ZonedDateTime} such as {@code "2007-12-03T10:15:30-03:00[America/Sao_Paulo]"}</li>
* <li>{@code java.util.Date} such as {@code "2007-12-03T10:15:30+01:00"}</li>
* </ul>
*
* @author oswaldo.bapvic.jr
* @since 1.2.0
*/
public class ParseFactory
{
private static final Map<Class<?>, Function<String, ?>> factory = new HashMap<>();
private static final Map<Class<?>, Function<String, ?>> PARSERS = new HashMap<>();

static
{
factory.put(Boolean.class, Boolean::valueOf);
factory.put(Byte.class, Byte::valueOf);
factory.put(Short.class, Short::valueOf);
factory.put(Integer.class, Integer::valueOf);
factory.put(Long.class, Long::valueOf);
factory.put(Float.class, Float::valueOf);
factory.put(Double.class, Double::valueOf);
factory.put(Character.class, string -> string.isEmpty() ? 0 : Character.valueOf(string.charAt(0)));
factory.put(String.class, Function.identity());
PARSERS.put(Boolean.class, Boolean::valueOf);
PARSERS.put(Byte.class, Byte::valueOf);
PARSERS.put(Short.class, Short::valueOf);
PARSERS.put(Integer.class, Integer::valueOf);
PARSERS.put(Long.class, Long::valueOf);
PARSERS.put(Float.class, Float::valueOf);
PARSERS.put(Double.class, Double::valueOf);
PARSERS.put(Character.class, string -> string.isEmpty() ? 0 : Character.valueOf(string.charAt(0)));
PARSERS.put(String.class, Function.identity());

// Legacy java.util.Date may accept the either of the formats
// "2007-12-03T10:15:30+01:00", or "2007-12-03T09:15:30Z"
PARSERS.put(java.util.Date.class, ParseFactory::parseDate);

// java.sql.Date, such as: "2007-2-28" or "2005-12-1"
PARSERS.put(java.sql.Date.class, java.sql.Date::valueOf);
// java.sql.Timestamp, such as: "2007-12-03 09:15:30.99"
PARSERS.put(Timestamp.class, Timestamp::valueOf);

// LocalDate, such as: "2007-12-03"
PARSERS.put(LocalDate.class, LocalDate::parse);
// LocalDateTime, such as: "2007-12-03T10:15:30"
PARSERS.put(LocalDateTime.class, LocalDateTime::parse);
// OffsetDateTime, such as: "2007-12-03T10:15:30+01:00"
PARSERS.put(OffsetDateTime.class, OffsetDateTime::parse);
// ZonedDateTime, such as: "2007-12-03T10:15:30+01:00[Europe/Paris]"
PARSERS.put(ZonedDateTime.class, ZonedDateTime::parse);
// Instant, such as: "2007-12-03T10:15:30.00Z"
PARSERS.put(Instant.class, Instant::parse);
// Duration, such as: "PT15M" (15 minutes)
PARSERS.put(Duration.class, Duration::parse);

PARSERS.put(Month.class, Month::valueOf);
PARSERS.put(DayOfWeek.class, DayOfWeek::valueOf);
}

// A custom DateTimeFormatter is required while Java 8 is still supported
private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME).optionalStart()
.appendOffset("+HH:MM", "Z") // The format +HH:MM is not standard in Java 8
.optionalEnd().toFormatter();

/**
* Private constructor to hide the public, implicit one.
*/
Expand All @@ -63,28 +117,39 @@ private ParseFactory()
* @param string the string to be parsed
* @return an object containing the result of the parsing of the specified string into the
* specified type
* @throws UnsupportedOperationException if the requested target type is not supported
* @throws UnsupportedOperationException if the specified type is not supported
*/
@SuppressWarnings("unchecked")
public static <T> T parse(Class<T> type, String string)
{
Class<?> objectType = ClassUtils.primitiveToWrapper(type);
Function<String, ?> function = getFunction(objectType)
.orElseThrow(() -> new UnsupportedOperationException("Unsupported type: " + type));
Object object = function.apply(string);
Function<String, ?> parser = getParser(objectType);
Object object = parser.apply(string);
return (T) object;
}

/**
* Returns an {@link Optional} possibly containing a {@link Function} to parse the
* specified target type.
* Returns a {@link Function} to parse the specified type.
*
* @param type the target type
* @return the {@link Function} to be applied for the specified type, or
* {@link Optional#empty()} if the specified type is not supported
* @return the {@link Function} to be applied for the specified type; not null
* @throws UnsupportedOperationException if the specified type is not supported
*/
private static Optional<Function<String, ?>> getFunction(Class<?> type)
private static Function<String, ?> getParser(Class<?> type)
{
return Optional.ofNullable(factory.get(type));
Function<String, ?> parser = PARSERS.get(type);
if (parser == null)
{
throw new UnsupportedOperationException("Unsupported type: " + type);
}
return parser;
}

private static Date parseDate(String string)
{
TemporalAccessor temporalAccessor = DATE_FORMAT.parse(string);
Instant instant = Instant.from(temporalAccessor);
return Date.from(instant);
}
oswaldobapvicjr marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.MatcherAssert.assertThat;

import java.sql.Timestamp;
import java.time.*;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
Expand All @@ -32,10 +40,44 @@
*/
class ParseFactoryTest
{

private static final String STR_UTC = "UTC";

private static final String STR_TRUE = "true";
private static final String STR_123 = "123";
private static final String STR_A = "A";

private static final String DATE_2022_12_03 = "2022-12-03";
private static final String DATE_2022_12_03_10_15_30 = "2022-12-03 10:15:30";
private static final String DATE_2022_12_03T10_15_30 = "2022-12-03T10:15:30";
private static final String DATE_2022_12_03T10_15_30_MINUS_03_00 = "2022-12-03T10:15:30-03:00";
private static final String DATE_2022_12_03T10_15_30_MINUS_03_00_AMERICA_SP = "2022-12-03T10:15:30-03:00[America/Sao_Paulo]";
private static final String DATE_2022_12_03T13_15_30Z = "2022-12-03T13:15:30Z";

private static final Date DATE_2023_12_03T13_15_30Z_AS_DATE = toDateUtc(2022, 12, 03, 13, 15, 30, 0);
private static final long DATE_2022_12_03T13_15_30Z_TIMESTAMP = DATE_2023_12_03T13_15_30Z_AS_DATE.getTime();

@BeforeAll
public static void setup()
{
Locale.setDefault(Locale.UK);
TimeZone.setDefault(TimeZone.getTimeZone(STR_UTC));
}

private static Date toDateUtc(int year, int month, int day, int hour, int minute, int second, int millisecond)
{
return toCalendarUtc(year, month, day, hour, minute, second, millisecond).getTime();
}

private static Calendar toCalendarUtc(int year, int month, int day, int hour, int minute, int second, int millisecond)
{
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(STR_UTC));
calendar.set(year, month - 1, day, hour, minute, second);
calendar.set(Calendar.MILLISECOND, millisecond);
return calendar;
}


@Test
void constructor_instantiationNotAllowed()
{
Expand Down Expand Up @@ -83,4 +125,85 @@ void parse_characterEmptyString_zero()
assertThat(ParseFactory.parse(Character.class, ""), equalTo('\0'));
}

@Test
void parse_localDate_success()
{
assertThat(ParseFactory.parse(LocalDate.class, DATE_2022_12_03),
equalTo(LocalDate.of(2022, 12, 3)));
}

@Test
void parse_localDateTime_success()
{
assertThat(ParseFactory.parse(LocalDateTime.class, DATE_2022_12_03T10_15_30),
equalTo(LocalDateTime.of(2022, 12, 3, 10, 15, 30, 0)));
}

@Test
void parse_offsetDateTime_success()
{
assertThat(ParseFactory.parse(OffsetDateTime.class, DATE_2022_12_03T10_15_30_MINUS_03_00),
equalTo(OffsetDateTime.of(2022, 12, 3, 10, 15, 30, 0, ZoneOffset.ofHours(-3))));
}

@Test
void parse_zonedDateTime_success()
{
assertThat(ParseFactory.parse(ZonedDateTime.class, DATE_2022_12_03T10_15_30_MINUS_03_00_AMERICA_SP),
equalTo(ZonedDateTime.of(2022, 12, 3, 10, 15, 30, 0, ZoneId.of("America/Sao_Paulo"))));
}

@Test
void parse_instant_success()
{
assertThat(ParseFactory.parse(Instant.class, DATE_2022_12_03T13_15_30Z),
equalTo(Instant.ofEpochMilli(DATE_2022_12_03T13_15_30Z_TIMESTAMP)));
}

@Test
void parse_duration_success()
{
assertThat(ParseFactory.parse(Duration.class, "PT15M"), equalTo(Duration.ofMinutes(15)));
}

@Test
void parse_javaUtilDateWithOffset_success()
{
assertThat(ParseFactory.parse(Date.class, DATE_2022_12_03T10_15_30_MINUS_03_00),
equalTo(DATE_2023_12_03T13_15_30Z_AS_DATE));
}

@Test
void parse_javaUtilDateZulu_success()
{
assertThat(ParseFactory.parse(Date.class, DATE_2022_12_03T13_15_30Z),
equalTo(DATE_2023_12_03T13_15_30Z_AS_DATE));
}

@Test
void parse_javaSqlDate_success()
{
assertThat(ParseFactory.parse(java.sql.Date.class, DATE_2022_12_03),
equalTo(toDateUtc(2022, 12, 3, 0, 0, 0, 0)));
}

@Test
void parse_timestamp_success()
{
assertThat(ParseFactory.parse(Timestamp.class, DATE_2022_12_03_10_15_30),
equalTo(new Timestamp(toDateUtc(2022, 12, 3, 10, 15, 30, 0).getTime())));
}

@Test
void parse_month_success()
{
assertThat(ParseFactory.parse(Month.class, "JULY"), equalTo(Month.JULY));
}

@Test
void parse_dayOfWeek_success()
{
assertThat(ParseFactory.parse(DayOfWeek.class, "FRIDAY"), equalTo(DayOfWeek.FRIDAY));
}

}