Finalize new period start and end mutating logic. Still need to update some existing tests and address TODOs.

This commit is contained in:
Luke deGruchy 2024-09-26 15:29:42 -04:00
parent 7fbdf30b72
commit b5a18002c3
5 changed files with 248 additions and 509 deletions

View File

@ -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

View File

@ -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;
};
}
}

View File

@ -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;
}

View File

@ -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 dont already, Ill return a 400 if they pass a start thats 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) {

View File

@ -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"))
);
}