Finalize new period start and end mutating logic. Still need to update some existing tests and address TODOs.
This commit is contained in:
parent
7fbdf30b72
commit
b5a18002c3
|
@ -21,7 +21,6 @@ package ca.uhn.fhir.util;
|
|||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import com.google.common.base.Preconditions;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
@ -31,19 +30,18 @@ import java.text.ParseException;
|
|||
import java.text.ParsePosition;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.Month;
|
||||
import java.time.YearMonth;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.time.temporal.TemporalField;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.TimeZone;
|
||||
|
||||
|
@ -105,25 +103,17 @@ public final class DateUtils {
|
|||
*/
|
||||
private DateUtils() {}
|
||||
|
||||
// LUKETODO: javadoc
|
||||
public static String formatWithZoneAndOptionalDelta(
|
||||
LocalDateTime localDateTime,
|
||||
ZoneId theZoneId,
|
||||
DateTimeFormatter theDateTimeFormatter,
|
||||
@Nullable TemporalAmount theDelta) {
|
||||
final ZonedDateTime zonedDateTime = localDateTime.atZone(theZoneId);
|
||||
|
||||
final ZonedDateTime zonedDateTimeToFormat =
|
||||
Optional.ofNullable(theDelta).map(zonedDateTime::plus).orElse(zonedDateTime);
|
||||
|
||||
return zonedDateTimeToFormat.format(theDateTimeFormatter);
|
||||
}
|
||||
|
||||
// LUKETODO: javadoc
|
||||
public static Optional<LocalDateTime> extractLocalDateTimeIfValid(TemporalAccessor theTemporalAccessor) {
|
||||
/**
|
||||
* Calculate a LocalDateTime with any missing date/time data points defaulting to the earliest values (ex 0 for hour)
|
||||
* from a TemporalAccessor or empty if it doesn't contain a year.
|
||||
*
|
||||
* @param theTemporalAccessor The TemporalAccessor containing date/time information
|
||||
* @return A LocalDateTime or empty
|
||||
*/
|
||||
public static Optional<LocalDateTime> extractLocalDateTimeForRangeStartOrEmpty(TemporalAccessor theTemporalAccessor) {
|
||||
if (theTemporalAccessor.isSupported(ChronoField.YEAR)) {
|
||||
final int year = theTemporalAccessor.get(ChronoField.YEAR);
|
||||
final int month = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MONTH_OF_YEAR, 1);
|
||||
final Month month = Month.of(getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MONTH_OF_YEAR, 1));
|
||||
final int day = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.DAY_OF_MONTH, 1);
|
||||
final int hour = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.HOUR_OF_DAY, 0);
|
||||
final int minute = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MINUTE_OF_HOUR, 0);
|
||||
|
@ -135,9 +125,44 @@ public final class DateUtils {
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
// LUKETODO: javadoc
|
||||
/**
|
||||
* Calculate a LocalDateTime with any missing date/time data points defaulting to the latest values (ex 23 for hour)
|
||||
* from a TemporalAccessor or empty if it doesn't contain a year.
|
||||
*
|
||||
* @param theTemporalAccessor The TemporalAccessor containing date/time information
|
||||
* @return A LocalDateTime or empty
|
||||
*/
|
||||
public static Optional<LocalDateTime> extractLocalDateTimeForRangeEndOrEmpty(TemporalAccessor theTemporalAccessor) {
|
||||
if (theTemporalAccessor.isSupported(ChronoField.YEAR)) {
|
||||
final int year = theTemporalAccessor.get(ChronoField.YEAR);
|
||||
final Month month = Month.of(getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MONTH_OF_YEAR, 12));
|
||||
final int day = getTimeUnitIfSupported(
|
||||
theTemporalAccessor,
|
||||
ChronoField.DAY_OF_MONTH,
|
||||
YearMonth.of(year, month).atEndOfMonth().getDayOfMonth()
|
||||
);
|
||||
final int hour = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.HOUR_OF_DAY, 23);
|
||||
final int minute = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.MINUTE_OF_HOUR, 59);
|
||||
final int seconds = getTimeUnitIfSupported(theTemporalAccessor, ChronoField.SECOND_OF_MINUTE, 59);
|
||||
|
||||
return Optional.of(LocalDateTime.of(year, month, day, hour, minute, seconds));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* With the provided DateTimeFormatter, parse a date time String or return empty if the String doesn't correspond
|
||||
* to the formatter.
|
||||
*
|
||||
* @param theDateTimeString A date/time String in some date format
|
||||
* @param theSupportedDateTimeFormatter The DateTimeFormatter we expect corresponds to the String
|
||||
* @return The parsed TemporalAccessor or empty
|
||||
*/
|
||||
public static Optional<TemporalAccessor> parseDateTimeStringIfValid(
|
||||
String theDateTimeString, DateTimeFormatter theSupportedDateTimeFormatter) {
|
||||
Objects.requireNonNull(theSupportedDateTimeFormatter);
|
||||
Preconditions.checkArgument(StringUtils.isNotBlank(theDateTimeString));
|
||||
|
||||
try {
|
||||
|
@ -147,24 +172,21 @@ public final class DateUtils {
|
|||
}
|
||||
}
|
||||
|
||||
// LUKETODO: javadoc
|
||||
public static Optional<ZoneOffset> getZoneOffsetIfSupported(TemporalAccessor theTemporalAccessor) {
|
||||
if (theTemporalAccessor.isSupported(ChronoField.OFFSET_SECONDS)) {
|
||||
return Optional.of(ZoneOffset.from(theTemporalAccessor));
|
||||
private static int getTimeUnitIfSupported(
|
||||
TemporalAccessor theTemporalAccessor, TemporalField theTemporalField, int theDefaultValue) {
|
||||
return getTimeUnitIfSupportedOrEmpty(theTemporalAccessor, theTemporalField)
|
||||
.orElse(theDefaultValue);
|
||||
}
|
||||
|
||||
private static Optional<Integer> getTimeUnitIfSupportedOrEmpty(
|
||||
TemporalAccessor theTemporalAccessor, TemporalField theTemporalField) {
|
||||
if (theTemporalAccessor.isSupported(theTemporalField)) {
|
||||
return Optional.of(theTemporalAccessor.get(theTemporalField));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static int getTimeUnitIfSupported(
|
||||
TemporalAccessor theTemporalAccessor, TemporalField theTemporalField, int theDefaultValue) {
|
||||
if (theTemporalAccessor.isSupported(theTemporalField)) {
|
||||
return theTemporalAccessor.get(theTemporalField);
|
||||
}
|
||||
|
||||
return theDefaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory for {@link SimpleDateFormat}s. The instances are stored in a
|
||||
* threadlocal way because SimpleDateFormat is not thread safe as noted in
|
||||
|
|
|
@ -1,438 +1,183 @@
|
|||
package ca.uhn.fhir.util;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.Month;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.time.temporal.TemporalField;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DateUtilsTest {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(DateUtilsTest.class);
|
||||
|
||||
private static final ZoneId TIMEZONE_NEWFOUNDLAND = ZoneId.of("America/St_Johns");
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
private static final Duration MINUS_ONE_SECOND = Duration.of(-1, ChronoUnit.SECONDS);
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY = DateTimeFormatter.ofPattern("yyyy");
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM = DateTimeFormatter.ofPattern("yyyy-MM");
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD = DateTimeFormatter.ISO_DATE;
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_HH = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH");
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm");
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
|
||||
|
||||
private static Stream<Arguments> extractLocalDateTimeIfValidParams() {
|
||||
private static Stream<Arguments> extractLocalDateTimeStartIfValidParams() {
|
||||
return Stream.of(
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2024),
|
||||
getTemporalAccessor("2024"),
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2023, 2),
|
||||
getTemporalAccessor("2023-02"),
|
||||
LocalDateTime.of(2023, Month.FEBRUARY, 1, 0, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2022, 9),
|
||||
getTemporalAccessor("2022-09"),
|
||||
LocalDateTime.of(2022, Month.SEPTEMBER, 1, 0, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2021, 3, 24),
|
||||
getTemporalAccessor("2021-03-24"),
|
||||
LocalDateTime.of(2021, Month.MARCH, 24, 0, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2024, 10, 23),
|
||||
getTemporalAccessor("2024-10-23"),
|
||||
LocalDateTime.of(2024, Month.OCTOBER, 23, 0, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2024, 8, 24, 12),
|
||||
getTemporalAccessor("2024-08-24T12"),
|
||||
LocalDateTime.of(2024, Month.AUGUST, 24, 12, 0, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2024, 11, 24, 12, 35),
|
||||
getTemporalAccessor("2024-11-24T12:35"),
|
||||
LocalDateTime.of(2024, Month.NOVEMBER, 24, 12, 35, 0)
|
||||
),
|
||||
Arguments.of(
|
||||
getTemporalAccessor(2024, 9, 24, 12, 35, 47),
|
||||
getTemporalAccessor("2024-09-24T12:35:47"),
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> formatWithZoneAndOptionalDeltaParams() {
|
||||
private static Stream<Arguments> extractLocalDateTimeEndIfValidParams() {
|
||||
return Stream.of(
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-01-01T00:00:00Z"
|
||||
getTemporalAccessor("2024"),
|
||||
LocalDateTime.of(2024, Month.DECEMBER, 31, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-02-01T00:00:00Z"
|
||||
getTemporalAccessor("2023-01"),
|
||||
LocalDateTime.of(2023, Month.JANUARY, 31, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-08-01T00:00:00Z"
|
||||
getTemporalAccessor("2023-02"),
|
||||
LocalDateTime.of(2023, Month.FEBRUARY, 28, 23, 59, 59)
|
||||
),
|
||||
// Leap year
|
||||
Arguments.of(
|
||||
getTemporalAccessor("2024-02"),
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 29, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-03-24T12:35:47Z"
|
||||
getTemporalAccessor("2023-03"),
|
||||
LocalDateTime.of(2023, Month.MARCH, 31, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-09-24T12:35:47Z"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2023-12-31T23:59:59Z"
|
||||
getTemporalAccessor("2023-04"),
|
||||
LocalDateTime.of(2023, Month.APRIL, 30, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-01-31T23:59:59Z"
|
||||
getTemporalAccessor("2023-05"),
|
||||
LocalDateTime.of(2023, Month.MAY, 31, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-07-31T23:59:59Z"
|
||||
getTemporalAccessor("2023-06"),
|
||||
LocalDateTime.of(2023, Month.JUNE, 30, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-03-24T12:35:46Z"
|
||||
getTemporalAccessor("2023-07"),
|
||||
LocalDateTime.of(2023, Month.JULY, 31, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-09-24T12:35:46Z"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-01-01T00:00:00-03:30"
|
||||
getTemporalAccessor("2023-08"),
|
||||
LocalDateTime.of(2023, Month.AUGUST, 31, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-02-01T00:00:00-03:30"
|
||||
getTemporalAccessor("2023-09"),
|
||||
LocalDateTime.of(2023, Month.SEPTEMBER, 30, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-08-01T00:00:00-02:30"
|
||||
getTemporalAccessor("2022-10"),
|
||||
LocalDateTime.of(2022, Month.OCTOBER, 31, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-03-24T12:35:47-02:30"
|
||||
getTemporalAccessor("2022-11"),
|
||||
LocalDateTime.of(2022, Month.NOVEMBER, 30, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-09-24T12:35:47-02:30"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2023-12-31T23:59:59-03:30"
|
||||
getTemporalAccessor("2022-12"),
|
||||
LocalDateTime.of(2022, Month.DECEMBER, 31, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-01-31T23:59:59-03:30"
|
||||
getTemporalAccessor("2021-03-24"),
|
||||
LocalDateTime.of(2021, Month.MARCH, 24, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-07-31T23:59:59-02:30"
|
||||
getTemporalAccessor("2024-10-23"),
|
||||
LocalDateTime.of(2024, Month.OCTOBER, 23, 23, 59, 59)
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-03-24T12:35:46-02:30"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-09-24T12:35:46-02:30"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-01-01T00:00:00Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-02-01T00:00:00Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-08-01T00:00:00Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-03-24T12:35:47Z"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z,
|
||||
null,
|
||||
"2024-09-24T12:35:47Z"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2023-12-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-01-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-07-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-03-24"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
ZoneOffset.UTC,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-09-24"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
null,
|
||||
"2024-01-01"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
null,
|
||||
"2024-02-01"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
null,
|
||||
"2024-08-01"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
null,
|
||||
"2024-03-24"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
null,
|
||||
"2024-09-24"
|
||||
),
|
||||
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.JANUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2023-12-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.FEBRUARY, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-01-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.AUGUST, 1, 0, 0, 0),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-07-31"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.MARCH, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-03-24"
|
||||
),
|
||||
Arguments.of(
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47),
|
||||
TIMEZONE_NEWFOUNDLAND,
|
||||
DATE_TIME_FORMATTER_YYYY_MM_DD,
|
||||
MINUS_ONE_SECOND,
|
||||
"2024-09-24"
|
||||
getTemporalAccessor("2024-09-24T12:35:47"),
|
||||
LocalDateTime.of(2024, Month.SEPTEMBER, 24, 12, 35, 47)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("extractLocalDateTimeIfValidParams")
|
||||
void extractLocalDateTimeIfValid (
|
||||
@MethodSource("extractLocalDateTimeStartIfValidParams")
|
||||
void extractLocalDateTimeStartIfValid (
|
||||
TemporalAccessor theTemporalAccessor,
|
||||
@Nullable LocalDateTime theExpectedResult) {
|
||||
assertThat(DateUtils.extractLocalDateTimeIfValid(theTemporalAccessor))
|
||||
assertThat(DateUtils.extractLocalDateTimeForRangeStartOrEmpty(theTemporalAccessor))
|
||||
.isEqualTo(Optional.ofNullable(theExpectedResult));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("formatWithZoneAndOptionalDeltaParams")
|
||||
void formatWithZoneAndOptionalDelta (
|
||||
LocalDateTime theLocalDateTime,
|
||||
ZoneId theZoneId,
|
||||
DateTimeFormatter theDateTimeFormatter,
|
||||
@Nullable TemporalAmount theDelta,
|
||||
String theExpectedResult) {
|
||||
|
||||
assertThat(DateUtils.formatWithZoneAndOptionalDelta(theLocalDateTime, theZoneId, theDateTimeFormatter, theDelta))
|
||||
.isEqualTo(theExpectedResult);
|
||||
@MethodSource("extractLocalDateTimeEndIfValidParams")
|
||||
void extractLocalDateTimeEndIfValid (
|
||||
TemporalAccessor theTemporalAccessor,
|
||||
@Nullable LocalDateTime theExpectedResult) {
|
||||
assertThat(DateUtils.extractLocalDateTimeForRangeEndOrEmpty(theTemporalAccessor))
|
||||
.isEqualTo(Optional.ofNullable(theExpectedResult));
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(int theYear) {
|
||||
return getTemporalAccessor(theYear, null, null, null, null, null);
|
||||
private static TemporalAccessor getTemporalAccessor(String theDateTimeString) {
|
||||
final DateTimeFormatter dateTimeFormatter = getDateTimeFormatter(theDateTimeString);
|
||||
|
||||
assertThat(dateTimeFormatter)
|
||||
.withFailMessage("Cannot find DateTimeFormatter for: " + theDateTimeString)
|
||||
.isNotNull();
|
||||
|
||||
return DateUtils.parseDateTimeStringIfValid(
|
||||
theDateTimeString,
|
||||
dateTimeFormatter
|
||||
).orElseThrow(() -> new IllegalArgumentException("Unable to parse: " + theDateTimeString));
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(int theYear, int theMonth) {
|
||||
return getTemporalAccessor(theYear, theMonth, null, null, null, null);
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(int theYear, int theMonth, int theDay) {
|
||||
return getTemporalAccessor(theYear, theMonth, theDay, null, null, null);
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(int theYear, int theMonth, int theDay, int theHour) {
|
||||
return getTemporalAccessor(theYear, theMonth, theDay, theHour, null, null);
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(int theYear, int theMonth, int theDay, int theHour, int theMinute) {
|
||||
return getTemporalAccessor(theYear, theMonth, theDay, theHour, theMinute, null);
|
||||
}
|
||||
|
||||
private static TemporalAccessor getTemporalAccessor(
|
||||
int year,
|
||||
@Nullable Integer month,
|
||||
@Nullable Integer day,
|
||||
@Nullable Integer hour,
|
||||
@Nullable Integer minute,
|
||||
@Nullable Integer second) {
|
||||
final TemporalAccessor temporalAccessor = mock(TemporalAccessor.class);
|
||||
|
||||
mockAccessor(temporalAccessor, ChronoField.YEAR, year);
|
||||
mockAccessor(temporalAccessor, ChronoField.MONTH_OF_YEAR, month);
|
||||
mockAccessor(temporalAccessor, ChronoField.DAY_OF_MONTH, day);
|
||||
mockAccessor(temporalAccessor, ChronoField.HOUR_OF_DAY, hour);
|
||||
mockAccessor(temporalAccessor, ChronoField.MINUTE_OF_HOUR, minute);
|
||||
mockAccessor(temporalAccessor, ChronoField.SECOND_OF_MINUTE, second);
|
||||
|
||||
return temporalAccessor;
|
||||
}
|
||||
|
||||
private static void mockAccessor(TemporalAccessor theTemporalAccessor, TemporalField theTemporalField, @Nullable Integer theValue) {
|
||||
if (theValue != null) {
|
||||
when(theTemporalAccessor.isSupported(theTemporalField)).thenReturn(true);
|
||||
when(theTemporalAccessor.get(theTemporalField)).thenReturn(theValue);
|
||||
}
|
||||
private static DateTimeFormatter getDateTimeFormatter(String theDateTimeString) {
|
||||
return switch (theDateTimeString.length()) {
|
||||
case 4 -> DATE_TIME_FORMATTER_YYYY;
|
||||
case 7 -> DATE_TIME_FORMATTER_YYYY_MM;
|
||||
case 10 -> DATE_TIME_FORMATTER_YYYY_MM_DD;
|
||||
case 13 -> DATE_TIME_FORMATTER_YYYY_MM_DD_HH;
|
||||
case 16 -> DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM;
|
||||
case 19 -> DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,24 +19,30 @@
|
|||
*/
|
||||
package ca.uhn.fhir.cr.r4.measure;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
// LUKETODO: javadoc
|
||||
// TODO: LD: make this a record when hapi-fhir supports JDK 17
|
||||
public class MeasurePeriodForEvaluation {
|
||||
@Nullable
|
||||
private final String myPeriodStart;
|
||||
@Nullable
|
||||
private final String myPeriodEnd;
|
||||
|
||||
public MeasurePeriodForEvaluation(String thePeriodStart, String thePeriodEnd) {
|
||||
public MeasurePeriodForEvaluation(@Nullable String thePeriodStart, @Nullable String thePeriodEnd) {
|
||||
myPeriodStart = thePeriodStart;
|
||||
myPeriodEnd = thePeriodEnd;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPeriodStart() {
|
||||
return myPeriodStart;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPeriodEnd() {
|
||||
return myPeriodEnd;
|
||||
}
|
||||
|
|
|
@ -23,77 +23,36 @@ import ca.uhn.fhir.rest.api.Constants;
|
|||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.util.DateUtils;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.apache.logging.log4j.util.Strings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
// LUKETODO: copyright header
|
||||
// LUKETODO: changelog
|
||||
// LUKETODO: javadoc
|
||||
// LUKETODO: unit test
|
||||
/*
|
||||
according to the docs: periodStart and periodEnd support Dates (YYYY, YYYY-MM, or YYYY-MM-DD) and DateTimes (YYYY-MM-DDThh:mm:ss+zz:zz)
|
||||
if the user passes in:
|
||||
Header: America/Toronto
|
||||
periodStart “2024-09-25T12:00:00-06:00”
|
||||
which timezone do we respect, or do we convert the periodStart to EDT?
|
||||
also, what happens if they pass in start and end in different formats?
|
||||
periodStart “2024-09-25T12:00:00-06:00"
|
||||
periodEnd: “2024-10-01”
|
||||
also, if we don’t already, I’ll return a 400 if they pass a start that’s after an end
|
||||
*/
|
||||
|
||||
/*
|
||||
| request input | request timezone | delta | converted input to CR |
|
||||
| --------------| ---------------- | ------ | ------------------------- |
|
||||
| 2024-09-24 | null | null | 2024-09-24T00:00:00Z |
|
||||
| 2024-09-24 | UTC | null | 2024-09-24T00:00:00Z |
|
||||
| 2024-09-24 | Newfoundland | null | 2024-09-24T00:00:00-02:30 |
|
||||
| 2024-09-24 | Eastern | null | 2024-09-24T00:00:00-04:00 |
|
||||
| 2024-09-24 | Mountain | null | 2024-09-24T00:00:00-06:00 |
|
||||
| 2024-02-24 | null | null | 2024-02-24T00:00:00Z |
|
||||
| 2024-02-24 | UTC | null | 2024-02-24T00:00:00Z |
|
||||
| 2024-02-24 | Newfoundland | null | 2024-02-24T00:00:00-03:30 |
|
||||
| 2024-02-24 | Eastern | null | 2024-02-24T00:00:00-05:00 |
|
||||
| 2024-02-24 | Mountain | null | 2024-02-24T00:00:00-07:00 |
|
||||
| 2024-09-24 | null | -1 sec | 2024-09-24T23:00:59Z |
|
||||
| 2024-09-24 | UTC | -1 sec | 2024-09-24T23:59:59Z |
|
||||
| 2024-09-24 | Newfoundland | -1 sec | 2024-09-24T23:59:59-02:30 |
|
||||
| 2024-09-24 | Eastern | -1 sec | 2024-09-24T23:59:59-04:00 |
|
||||
| 2024-09-24 | Mountain | -1 sec | 2024-09-24T23:59:59-06:00 |
|
||||
| 2024-02-24 | null | -1 sec | 2024-02-24T23:59:59Z |
|
||||
| 2024-02-24 | UTC | -1 sec | 2024-02-24T23:59:59Z |
|
||||
| 2024-02-24 | Newfoundland | -1 sec | 2024-02-24T23:59:59-03:30 |
|
||||
| 2024-02-24 | Eastern | -1 sec | 2024-02-24T23:59:59-05:00 |
|
||||
| 2024-02-24 | Mountain | -1 sec | 2024-02-24T23:59:59-07:00 |
|
||||
*/
|
||||
public class MeasureReportPeriodRequestProcessingService {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(MeasureReportPeriodRequestProcessingService.class);
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_INPUT = DateTimeFormatter.ofPattern("yyyy");
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_INPUT = DateTimeFormatter.ofPattern("yyyy-MM");
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_INPUT = DateTimeFormatter.ISO_DATE;
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z_INPUT_OR_OUTPUT =
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_INPUT = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z_OUTPUT =
|
||||
DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
private static final Duration MINUS_ONE_SECOND = Duration.of(-1, ChronoUnit.SECONDS);
|
||||
|
||||
private static final Map<Integer, DateTimeFormatter> VALID_DATE_TIME_FORMATTERS_BY_FORMAT_LENGTH = Map.of(
|
||||
private static final Map<Integer, DateTimeFormatter> VALID_DATE_TIME_FORMATTERS_BY_FORMAT_LENGTH =
|
||||
Map.of(
|
||||
4, DATE_TIME_FORMATTER_YYYY_INPUT,
|
||||
7, DATE_TIME_FORMATTER_YYYY_MM_INPUT,
|
||||
10, DATE_TIME_FORMATTER_YYYY_MM_DD_INPUT,
|
||||
20, DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z_INPUT_OR_OUTPUT,
|
||||
25, DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z_INPUT_OR_OUTPUT);
|
||||
19, DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_INPUT
|
||||
);
|
||||
|
||||
private final ZoneId myFallbackTimezone;
|
||||
|
||||
|
@ -110,9 +69,9 @@ public class MeasureReportPeriodRequestProcessingService {
|
|||
|
||||
private MeasurePeriodForEvaluation validateInputDates(
|
||||
String thePeriodStart, String thePeriodEnd, ZoneId theZoneId) {
|
||||
|
||||
if (Strings.isBlank(thePeriodStart) || Strings.isBlank(thePeriodEnd)) {
|
||||
throw new InvalidRequestException(
|
||||
String.format("Either start: [%s] or end: [%s] or both are blank", thePeriodStart, thePeriodEnd));
|
||||
return new MeasurePeriodForEvaluation(null, null);
|
||||
}
|
||||
|
||||
if (thePeriodStart.length() != thePeriodEnd.length()) {
|
||||
|
@ -125,27 +84,18 @@ public class MeasureReportPeriodRequestProcessingService {
|
|||
|
||||
if (dateTimeFormatterStart == null) {
|
||||
throw new InvalidRequestException(String.format(
|
||||
"Either start: %s or end: %s or both have an supported date/time format",
|
||||
thePeriodStart, thePeriodEnd));
|
||||
}
|
||||
|
||||
final Optional<TemporalAccessor> optTemporalAccessorStart =
|
||||
DateUtils.parseDateTimeStringIfValid(thePeriodStart, dateTimeFormatterStart);
|
||||
final Optional<TemporalAccessor> optTemporalAccessorEnd =
|
||||
DateUtils.parseDateTimeStringIfValid(thePeriodEnd, dateTimeFormatterStart);
|
||||
|
||||
if (optTemporalAccessorStart.isEmpty() || optTemporalAccessorEnd.isEmpty()) {
|
||||
throw new InvalidRequestException("Either start or end period have an unsupported format");
|
||||
"Unsupported Date/Time format for period start: %s or end: %s",
|
||||
thePeriodStart, thePeriodEnd)
|
||||
);
|
||||
}
|
||||
|
||||
final Optional<LocalDateTime> optLocalDateTimeStart =
|
||||
DateUtils.extractLocalDateTimeIfValid(optTemporalAccessorStart.get());
|
||||
final Optional<LocalDateTime> optLocalDateTimeEnd =
|
||||
DateUtils.extractLocalDateTimeIfValid(optTemporalAccessorEnd.get());
|
||||
DateUtils.parseDateTimeStringIfValid(thePeriodStart, dateTimeFormatterStart)
|
||||
.flatMap(DateUtils::extractLocalDateTimeForRangeStartOrEmpty);
|
||||
|
||||
final Optional<ZoneOffset> optZoneOffsetStart =
|
||||
DateUtils.getZoneOffsetIfSupported(optTemporalAccessorStart.get());
|
||||
final Optional<ZoneOffset> optZoneOffsetEnd = DateUtils.getZoneOffsetIfSupported(optTemporalAccessorEnd.get());
|
||||
final Optional<LocalDateTime> optLocalDateTimeEnd =
|
||||
DateUtils.parseDateTimeStringIfValid(thePeriodEnd, dateTimeFormatterStart)
|
||||
.flatMap(DateUtils::extractLocalDateTimeForRangeEndOrEmpty);
|
||||
|
||||
if (optLocalDateTimeStart.isEmpty() || optLocalDateTimeEnd.isEmpty()) {
|
||||
throw new InvalidRequestException("Either start or end period have an unsupported format");
|
||||
|
@ -164,46 +114,16 @@ public class MeasureReportPeriodRequestProcessingService {
|
|||
String.format("Start date: %s is after end date: %s", thePeriodStart, thePeriodEnd));
|
||||
}
|
||||
|
||||
if (!optZoneOffsetStart.equals(optZoneOffsetEnd)) {
|
||||
throw new InvalidRequestException(String.format(
|
||||
"Zone offsets do not match for period start: %s and end: %s", thePeriodStart, thePeriodEnd));
|
||||
}
|
||||
|
||||
// Either the input Strings have no offset
|
||||
|
||||
// Or the request timezone is different from the default
|
||||
if (optZoneOffsetStart.isEmpty() || !myFallbackTimezone.getRules().equals(theZoneId.getRules())) {
|
||||
|
||||
// Preserve backwards compatibility
|
||||
if (optZoneOffsetStart.isPresent()) {
|
||||
ourLog.warn(
|
||||
"Start offset is not the same as the timezone header. Ignoring both start and stop offsets for start: {} amd: {}",
|
||||
thePeriodStart,
|
||||
thePeriodEnd);
|
||||
}
|
||||
|
||||
return new MeasurePeriodForEvaluation(
|
||||
formatWithZonePeriodStart(localDateTimeStart, theZoneId),
|
||||
formatWithZonePeriodEnd(localDateTimeEnd, theZoneId));
|
||||
}
|
||||
|
||||
return new MeasurePeriodForEvaluation(
|
||||
formatWithZonePeriodStart(localDateTimeStart, optZoneOffsetStart.get()),
|
||||
formatWithZonePeriodEnd(localDateTimeEnd, optZoneOffsetEnd.get()));
|
||||
formatWithTimezone(localDateTimeStart, theZoneId),
|
||||
formatWithTimezone(localDateTimeEnd, theZoneId)
|
||||
);
|
||||
}
|
||||
|
||||
private String formatWithZonePeriodStart(LocalDateTime theLocalDateTime, ZoneId theZoneId) {
|
||||
return formatWithZoneAndOptionalDelta(theLocalDateTime, theZoneId, null);
|
||||
}
|
||||
|
||||
private String formatWithZonePeriodEnd(LocalDateTime theLocalDateTime, ZoneId theZoneId) {
|
||||
return formatWithZoneAndOptionalDelta(theLocalDateTime, theZoneId, MINUS_ONE_SECOND);
|
||||
}
|
||||
|
||||
private String formatWithZoneAndOptionalDelta(
|
||||
LocalDateTime theLocalDateTime, ZoneId theZoneId, @Nullable TemporalAmount theDelta) {
|
||||
return DateUtils.formatWithZoneAndOptionalDelta(
|
||||
theLocalDateTime, theZoneId, DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z_INPUT_OR_OUTPUT, theDelta);
|
||||
private String formatWithTimezone(
|
||||
LocalDateTime theLocalDateTime, ZoneId theZoneId) {
|
||||
return theLocalDateTime.atZone(theZoneId)
|
||||
.format(DATE_TIME_FORMATTER_YYYY_MM_DD_HH_MM_SS_Z_OUTPUT);
|
||||
}
|
||||
|
||||
private ZoneId getClientTimezoneOrInvalidRequest(RequestDetails theRequestDetails) {
|
||||
|
|
|
@ -21,38 +21,74 @@ class MeasureReportPeriodRequestProcessingServiceTest {
|
|||
|
||||
private final MeasureReportPeriodRequestProcessingService myTestSubject = new MeasureReportPeriodRequestProcessingService(ZoneOffset.UTC);
|
||||
|
||||
// LUKETODO: what happens if only one is null?
|
||||
@ParameterizedTest
|
||||
@CsvSource( nullValues = {"null"},
|
||||
value={
|
||||
"null, 2020, 2021, 2020-01-01T00:00:00Z, 2020-12-31T23:59:59Z",
|
||||
"UTC, 2020, 2021, 2020-01-01T00:00:00Z, 2020-12-31T23:59:59Z",
|
||||
"America/St_Johns, 2020, 2021, 2020-01-01T00:00:00-03:30, 2020-12-31T23:59:59-03:30",
|
||||
"America/Toronto, 2020, 2021, 2020-01-01T00:00:00-05:00, 2020-12-31T23:59:59-05:00",
|
||||
"America/Denver, 2020, 2021, 2020-01-01T00:00:00-07:00, 2020-12-31T23:59:59-07:00",
|
||||
// request timezone start end expected converted start expected converted end
|
||||
"null, null, null, null, null",
|
||||
"Z, null, null, null, null",
|
||||
"UTC, null, null, null, null",
|
||||
"America/St_Johns, null, null, null, null",
|
||||
"America/Toronto, null, null, null, null",
|
||||
"America/Denver, null, null, null, null",
|
||||
|
||||
"null, 2022-02, 2022-08, 2022-02-01T00:00:00Z, 2022-07-31T23:59:59Z",
|
||||
"UTC, 2022-02, 2022-08, 2022-02-01T00:00:00Z, 2022-07-31T23:59:59Z",
|
||||
"America/St_Johns, 2022-02, 2022-08, 2022-02-01T00:00:00-03:30, 2022-07-31T23:59:59-02:30",
|
||||
"America/Toronto, 2022-02, 2022-08, 2022-02-01T00:00:00-05:00, 2022-07-31T23:59:59-04:00",
|
||||
"America/Denver, 2022-02, 2022-08, 2022-02-01T00:00:00-07:00, 2022-07-31T23:59:59-06:00",
|
||||
"null, 2020, 2021, 2020-01-01T00:00:00Z, 2021-12-31T23:59:59Z",
|
||||
"Z, 2020, 2021, 2020-01-01T00:00:00Z, 2021-12-31T23:59:59Z",
|
||||
"UTC, 2020, 2021, 2020-01-01T00:00:00Z, 2021-12-31T23:59:59Z",
|
||||
"America/St_Johns, 2020, 2021, 2020-01-01T00:00:00-03:30, 2021-12-31T23:59:59-03:30",
|
||||
"America/Toronto, 2020, 2021, 2020-01-01T00:00:00-05:00, 2021-12-31T23:59:59-05:00",
|
||||
"America/Denver, 2020, 2021, 2020-01-01T00:00:00-07:00, 2021-12-31T23:59:59-07:00",
|
||||
|
||||
"null, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00Z, 2024-02-25T23:59:59Z",
|
||||
"UTC, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00Z, 2024-02-25T23:59:59Z",
|
||||
"America/St_Johns, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00-03:30, 2024-02-25T23:59:59-03:30",
|
||||
"America/Toronto, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00-05:00, 2024-02-25T23:59:59-05:00",
|
||||
"America/Denver, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00-07:00, 2024-02-25T23:59:59-07:00",
|
||||
"null, 2022-02, 2022-08, 2022-02-01T00:00:00Z, 2022-08-31T23:59:59Z",
|
||||
"Z, 2022-02, 2022-08, 2022-02-01T00:00:00Z, 2022-08-31T23:59:59Z",
|
||||
"UTC, 2022-02, 2022-08, 2022-02-01T00:00:00Z, 2022-08-31T23:59:59Z",
|
||||
"America/St_Johns, 2022-02, 2022-08, 2022-02-01T00:00:00-03:30, 2022-08-31T23:59:59-02:30",
|
||||
"America/Toronto, 2022-02, 2022-08, 2022-02-01T00:00:00-05:00, 2022-08-31T23:59:59-04:00",
|
||||
"America/Denver, 2022-02, 2022-08, 2022-02-01T00:00:00-07:00, 2022-08-31T23:59:59-06:00",
|
||||
|
||||
"null, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00Z, 2024-09-25T23:59:59Z",
|
||||
"UTC, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00Z, 2024-09-25T23:59:59Z",
|
||||
"America/St_Johns, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00-02:30, 2024-09-25T23:59:59-02:30",
|
||||
"America/Toronto, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00-04:00, 2024-09-25T23:59:59-04:00",
|
||||
"America/Denver, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00-06:00, 2024-09-25T23:59:59-06:00",
|
||||
"null, 2022-02, 2022-02, 2022-02-01T00:00:00Z, 2022-02-28T23:59:59Z",
|
||||
"Z, 2022-02, 2022-02, 2022-02-01T00:00:00Z, 2022-02-28T23:59:59Z",
|
||||
"UTC, 2022-02, 2022-02, 2022-02-01T00:00:00Z, 2022-02-28T23:59:59Z",
|
||||
"America/St_Johns, 2022-02, 2022-02, 2022-02-01T00:00:00-03:30, 2022-02-28T23:59:59-03:30",
|
||||
"America/Toronto, 2022-02, 2022-02, 2022-02-01T00:00:00-05:00, 2022-02-28T23:59:59-05:00",
|
||||
"America/Denver, 2022-02, 2022-02, 2022-02-01T00:00:00-07:00, 2022-02-28T23:59:59-07:00",
|
||||
|
||||
"null, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00, 2024-09-25T12:00:00-06:00, 2024-09-26T11:59:59-06:00",
|
||||
"UTC, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00, 2024-09-25T12:00:00-06:00, 2024-09-26T11:59:59-06:00",
|
||||
"America/St_Johns, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00, 2024-09-25T12:00:00-02:30, 2024-09-26T11:59:59-02:30",
|
||||
"America/Toronto, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00, 2024-09-25T12:00:00-04:00, 2024-09-26T11:59:59-04:00",
|
||||
"America/Denver, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00, 2024-09-25T12:00:00-06:00, 2024-09-26T11:59:59-06:00",
|
||||
// Leap year
|
||||
"null, 2024-02, 2024-02, 2024-02-01T00:00:00Z, 2024-02-29T23:59:59Z",
|
||||
"Z, 2024-02, 2024-02, 2024-02-01T00:00:00Z, 2024-02-29T23:59:59Z",
|
||||
"UTC, 2024-02, 2024-02, 2024-02-01T00:00:00Z, 2024-02-29T23:59:59Z",
|
||||
"America/St_Johns, 2024-02, 2024-02, 2024-02-01T00:00:00-03:30, 2024-02-29T23:59:59-03:30",
|
||||
"America/Toronto, 2024-02, 2024-02, 2024-02-01T00:00:00-05:00, 2024-02-29T23:59:59-05:00",
|
||||
"America/Denver, 2024-02, 2024-02, 2024-02-01T00:00:00-07:00, 2024-02-29T23:59:59-07:00",
|
||||
|
||||
"null, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00Z, 2024-02-26T23:59:59Z",
|
||||
"Z, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00Z, 2024-02-26T23:59:59Z",
|
||||
"UTC, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00Z, 2024-02-26T23:59:59Z",
|
||||
"America/St_Johns, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00-03:30, 2024-02-26T23:59:59-03:30",
|
||||
"America/Toronto, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00-05:00, 2024-02-26T23:59:59-05:00",
|
||||
"America/Denver, 2024-02-25, 2024-02-26, 2024-02-25T00:00:00-07:00, 2024-02-26T23:59:59-07:00",
|
||||
|
||||
"null, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00Z, 2024-09-26T23:59:59Z",
|
||||
"Z, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00Z, 2024-09-26T23:59:59Z",
|
||||
"UTC, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00Z, 2024-09-26T23:59:59Z",
|
||||
"America/St_Johns, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00-02:30, 2024-09-26T23:59:59-02:30",
|
||||
"America/Toronto, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00-04:00, 2024-09-26T23:59:59-04:00",
|
||||
"America/Denver, 2024-09-25, 2024-09-26, 2024-09-25T00:00:00-06:00, 2024-09-26T23:59:59-06:00",
|
||||
|
||||
"null, 2024-01-01T12:00:00, 2024-01-02T12:00:00, 2024-01-01T12:00:00Z, 2024-01-02T12:00:00Z",
|
||||
"Z, 2024-01-01T12:00:00, 2024-01-02T12:00:00, 2024-01-01T12:00:00Z, 2024-01-02T12:00:00Z",
|
||||
"UTC, 2024-01-01T12:00:00, 2024-01-02T12:00:00, 2024-01-01T12:00:00Z, 2024-01-02T12:00:00Z",
|
||||
"America/St_Johns, 2024-01-01T12:00:00, 2024-01-02T12:00:00, 2024-01-01T12:00:00-03:30, 2024-01-02T12:00:00-03:30",
|
||||
"America/Toronto, 2024-01-01T12:00:00, 2024-01-02T12:00:00, 2024-01-01T12:00:00-05:00, 2024-01-02T12:00:00-05:00",
|
||||
"America/Denver, 2024-01-01T12:00:00, 2024-01-02T12:00:00, 2024-01-01T12:00:00-07:00, 2024-01-02T12:00:00-07:00",
|
||||
|
||||
"null, 2024-09-25T12:00:00, 2024-09-26T12:00:00, 2024-09-25T12:00:00Z, 2024-09-26T12:00:00Z",
|
||||
"Z, 2024-09-25T12:00:00, 2024-09-26T12:00:00, 2024-09-25T12:00:00Z, 2024-09-26T12:00:00Z",
|
||||
"UTC, 2024-09-25T12:00:00, 2024-09-26T12:00:00, 2024-09-25T12:00:00Z, 2024-09-26T12:00:00Z",
|
||||
"America/St_Johns, 2024-09-25T12:00:00, 2024-09-26T12:00:00, 2024-09-25T12:00:00-02:30, 2024-09-26T12:00:00-02:30",
|
||||
"America/Toronto, 2024-09-25T12:00:00, 2024-09-26T12:00:00, 2024-09-25T12:00:00-04:00, 2024-09-26T12:00:00-04:00",
|
||||
"America/Denver, 2024-09-25T12:00:00, 2024-09-26T12:00:00, 2024-09-25T12:00:00-06:00, 2024-09-26T12:00:00-06:00",
|
||||
}
|
||||
)
|
||||
void validateAndProcessTimezone_happyPath(@Nullable String theTimezone, String theInputPeriodStart, String theInputPeriodEnd, String theOutputPeriodStart, String theOutputPeriodEnd) {
|
||||
|
@ -64,16 +100,26 @@ class MeasureReportPeriodRequestProcessingServiceTest {
|
|||
assertThat(actualResult).isEqualTo(expectedResult);
|
||||
}
|
||||
|
||||
public static Stream<Arguments> errorParams() {
|
||||
private static Stream<Arguments> errorParams() {
|
||||
return Stream.of(
|
||||
Arguments.of(null, null, null, new InvalidRequestException("Either start: [null] or end: [null] or both are blank")),
|
||||
Arguments.of(null, "", "", new InvalidRequestException("Either start: [] or end: [] or both are blank")),
|
||||
Arguments.of(null, "2024", "2024-01", new InvalidRequestException("Period start: 2024 and end: 2024-01 are not the same date/time formats")),
|
||||
Arguments.of(null, "2024-01-01T12", "2024-01-01T12", new InvalidRequestException("Either start: 2024-01-01T12 or end: 2024-01-01T12 or both have an supported date/time format")),
|
||||
Arguments.of(null, "2024-01-01T12:00:00-02:30", "2024-01-02T12:00:00-04:00", new InvalidRequestException("Zone offsets do not match for period start: 2024-01-01T12:00:00-02:30 and end: 2024-01-02T12:00:00-04:00")),
|
||||
Arguments.of(null, "2024-01-01", "2024-01-01", new InvalidRequestException("Start date: 2024-01-01 is the same as end date: 2024-01-01")),
|
||||
Arguments.of(null, "2024-01-01T12", "2024-01-01T12", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-01-01T12 or end: 2024-01-01T12")),
|
||||
Arguments.of(null, "2024-01-02", "2024-01-01", new InvalidRequestException("Start date: 2024-01-02 is after end date: 2024-01-01")),
|
||||
Arguments.of("Middle-Earth/Combe", "2024-01-02", "2024-01-03", new InvalidRequestException("Invalid value for Timezone header: Middle-Earth/Combe"))
|
||||
Arguments.of("Middle-Earth/Combe", "2024-01-02", "2024-01-03", new InvalidRequestException("Invalid value for Timezone header: Middle-Earth/Combe")),
|
||||
Arguments.of(null, "2024-01-01T12:00:00-02:30", "2024-01-02T12:00:00-04:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-01-01T12:00:00-02:30 or end: 2024-01-02T12:00:00-04:00")),
|
||||
Arguments.of("Z", "2024-01-01T12:00:00-02:30", "2024-01-02T12:00:00-04:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-01-01T12:00:00-02:30 or end: 2024-01-02T12:00:00-04:00")),
|
||||
Arguments.of("UTC", "2024-01-01T12:00:00-02:30", "2024-01-02T12:00:00-04:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-01-01T12:00:00-02:30 or end: 2024-01-02T12:00:00-04:00")),
|
||||
Arguments.of("America/St_Johns", "2024-01-01T12:00:00-02:30", "2024-01-02T12:00:00-04:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-01-01T12:00:00-02:30 or end: 2024-01-02T12:00:00-04:00")),
|
||||
Arguments.of("America/Toronto", "2024-01-01T12:00:00-02:30", "2024-01-02T12:00:00-04:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-01-01T12:00:00-02:30 or end: 2024-01-02T12:00:00-04:00")),
|
||||
Arguments.of("America/Denver", "2024-01-01T12:00:00-02:30", "2024-01-02T12:00:00-04:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-01-01T12:00:00-02:30 or end: 2024-01-02T12:00:00-04:00")),
|
||||
Arguments.of(null, "2024-09-25T12:00:00-06:00", "2024-09-26T12:00:00-06:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-09-25T12:00:00-06:00 or end: 2024-09-26T12:00:00-06:00")),
|
||||
Arguments.of("Z", "2024-09-25T12:00:00-06:00", "2024-09-26T12:00:00-06:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-09-25T12:00:00-06:00 or end: 2024-09-26T12:00:00-06:00")),
|
||||
Arguments.of("UTC", "2024-09-25T12:00:00-06:00", "2024-09-26T12:00:00-06:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-09-25T12:00:00-06:00 or end: 2024-09-26T12:00:00-06:00")),
|
||||
Arguments.of("America/St_Johns", "2024-09-25T12:00:00-06:00", "2024-09-26T12:00:00-06:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-09-25T12:00:00-06:00 or end: 2024-09-26T12:00:00-06:00")),
|
||||
Arguments.of("America/Toronto", "2024-09-25T12:00:00-06:00", "2024-09-26T12:00:00-06:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-09-25T12:00:00-06:00 or end: 2024-09-26T12:00:00-06:00")),
|
||||
Arguments.of("America/Denver", "2024-09-25T12:00:00-06:00", "2024-09-26T12:00:00-06:00", new InvalidRequestException("Unsupported Date/Time format for period start: 2024-09-25T12:00:00-06:00 or end: 2024-09-26T12:00:00-06:00"))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue