From 275173410bf03398104f00f8e0f495572053b2e8 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 27 Oct 2023 18:31:43 -0400 Subject: [PATCH] Fix DateTime initialization and comparison code to deal correctly with half hour and fifteen minute timezones (#1259) * Add test to run on Newfoundland TZ machine. * Copy all 4 tests failing on the Newfoundland laptop into the timezone CQL file. * Copy all 4 tests failing on the Newfoundland laptop into the timezone CQL file. * Change Newfoundland tests to to SoftAssert. * Ensure all tests fail in Newfoundland. * Fix wrong test name. * Successfully set timezone within a unit test and replicated the expected failures on an EST/EDT workstation. * Parameterized test with different timezones. * Bugfix that works on standard timezone workstation. * Add parameterized tests for CqlTypesOperatorsTest, which fails on the Newfoundland workstation due a flaw in the bugfix. * Instantiate ZoneId in DateTime from all constructors. Add getter. Add two getNormalized() methods, one of which takes another ZoneId. Pass in the "this" DateTime's zoneId when normalizing the "other" DateTime. Add stub with comments for new DateTimeTest. Lots of TODOs and cleanup needed. Need to test on Newfoundland workstation. * Fix case in test CQL file to fix pipeline failure. Cleanup code in DateTime. * Add tests to new unit test. Leave open the question of how to handle DST. TODO: 2 param getNormalized(). * More tweaks. Try to optimize the performance of DateTime. Still haven't figured out why Newfoundland in non-DST fails. * Comment out experimental code that may impact performance. * Stop using ZoneId and use ZoneOffset instead. This seems to resolve the Newfoundland non DST bug. * Remove all remnants of old ZoneId code. Add more unit tests. * More cleanup. --- .../cqf/cql/engine/runtime/DateTime.java | 48 ++- .../engine/execution/CqlMainSuiteTest.java | 21 + .../cqf/cql/engine/execution/CqlTestBase.java | 16 + .../engine/execution/CqlTimezoneTests.java | 42 ++ .../execution/CqlTypesOperatorsTest.java | 371 +++++++++--------- .../cqf/cql/engine/runtime/DateTimeTest.java | 201 ++++++++++ .../engine/execution/CqlTimeZoneTestSuite.cql | 172 ++++++++ .../cql/engine/execution/CqlTimezoneTests.cql | 6 + 8 files changed, 677 insertions(+), 200 deletions(-) create mode 100644 Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTimezoneTests.java create mode 100644 Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/runtime/DateTimeTest.java create mode 100644 Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTimeZoneTestSuite.cql create mode 100644 Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTimezoneTests.cql diff --git a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/runtime/DateTime.java b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/runtime/DateTime.java index 60941692a..dc6a1805d 100644 --- a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/runtime/DateTime.java +++ b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/runtime/DateTime.java @@ -1,5 +1,7 @@ package org.opencds.cqf.cql.engine.runtime; +import org.opencds.cqf.cql.engine.exception.InvalidDateTime; + import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.OffsetDateTime; @@ -8,10 +10,8 @@ import java.util.Date; import java.util.TimeZone; -import org.opencds.cqf.cql.engine.exception.InvalidDateTime; -import org.opencds.cqf.cql.engine.execution.State; - public class DateTime extends BaseTemporal { + private final ZoneOffset zoneOffset; private OffsetDateTime dateTime; public OffsetDateTime getDateTime() { @@ -40,14 +40,18 @@ public DateTime withPrecision(Precision precision) { public DateTime(OffsetDateTime dateTime) { setDateTime(dateTime); this.precision = Precision.MILLISECOND; + zoneOffset = toZoneOffset(dateTime); } public DateTime(OffsetDateTime dateTime, Precision precision) { setDateTime(dateTime); this.precision = precision; + zoneOffset = toZoneOffset(dateTime); } public DateTime(String dateString, ZoneOffset offset) { + zoneOffset = offset; + // Handles case when Tz is not complete (T02:04:59.123+01) if (dateString.matches("T[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d{3}(\\+|-)\\d{2}$")) { dateString += ":00"; @@ -98,6 +102,8 @@ public DateTime(BigDecimal offset, int ... dateElements) { throw new InvalidDateTime("DateTime must include a year"); } + zoneOffset = toZoneOffset(offset); + StringBuilder dateString = new StringBuilder(); String[] stringElements = TemporalHelper.normalizeDateTimeElements(dateElements); @@ -128,13 +134,16 @@ else if (i == 6) { // Otherwise, parse as a LocalDateTime and then interpret that in the evaluation timezone if (offset != null) { - dateString.append(ZoneOffset.ofHoursMinutes(offset.intValue(), new BigDecimal("60").multiply(offset.remainder(BigDecimal.ONE)).intValue()).getId()); + dateString.append(toZoneOffset(offset).getId()); setDateTime(OffsetDateTime.parse(dateString.toString())); } else { setDateTime(TemporalHelper.toOffsetDateTime(LocalDateTime.parse(dateString.toString()))); } + } + public ZoneOffset getZoneOffset() { + return zoneOffset; } public DateTime expandPartialMinFromPrecision(Precision thePrecision) { @@ -204,10 +213,14 @@ public Integer compare(BaseTemporal other, boolean forSort) { } } - public OffsetDateTime getNormalized(Precision precision, State c) { + public OffsetDateTime getNormalized(Precision precision) { + return getNormalized(precision, zoneOffset); + } + + public OffsetDateTime getNormalized(Precision precision, ZoneOffset nullableZoneOffset) { if (precision.toDateTimeIndex() > Precision.DAY.toDateTimeIndex()) { - if (c != null) { - return dateTime.atZoneSameInstant(c.getEvaluationZonedDateTime().getZone()).toOffsetDateTime(); + if (nullableZoneOffset != null) { + return dateTime.withOffsetSameInstant(nullableZoneOffset); } return dateTime.atZoneSameInstant(TimeZone.getDefault().toZoneId()).toOffsetDateTime(); @@ -216,10 +229,6 @@ public OffsetDateTime getNormalized(Precision precision, State c) { return dateTime; } - public OffsetDateTime getNormalized(Precision precision) { - return getNormalized(precision, null); - } - @Override public Integer compareToPrecision(BaseTemporal other, Precision thePrecision) { boolean leftMeetsPrecisionRequirements = this.precision.toDateTimeIndex() >= thePrecision.toDateTimeIndex(); @@ -227,7 +236,7 @@ public Integer compareToPrecision(BaseTemporal other, Precision thePrecision) { // adjust dates to evaluation offset OffsetDateTime leftDateTime = this.getNormalized(thePrecision); - OffsetDateTime rightDateTime = ((DateTime) other).getNormalized(thePrecision); + OffsetDateTime rightDateTime = ((DateTime) other).getNormalized(thePrecision, getZoneOffset()); if (!leftMeetsPrecisionRequirements || !rightMeetsPrecisionRequirements) { thePrecision = Precision.getLowestDateTimePrecision(this.precision, other.precision); @@ -292,4 +301,19 @@ public static DateTime fromJavaDate(Date date) { calendar.setTime(date); return new DateTime(OffsetDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId()), Precision.MILLISECOND); } + + private ZoneOffset toZoneOffset(OffsetDateTime offsetDateTime) { + return offsetDateTime.getOffset(); + } + + private ZoneOffset toZoneOffset(BigDecimal offsetAsBigDecimal) { + if (offsetAsBigDecimal == null) { + return null; + } + + return ZoneOffset.ofHoursMinutes(offsetAsBigDecimal.intValue(), + new BigDecimal(60) + .multiply(offsetAsBigDecimal.remainder(BigDecimal.ONE)) + .intValue()); + } } diff --git a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlMainSuiteTest.java b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlMainSuiteTest.java index 49dd43619..76643c651 100644 --- a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlMainSuiteTest.java +++ b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlMainSuiteTest.java @@ -42,6 +42,27 @@ public void test_all_portable_cql_engine_tests() { } + @Test + public void test_cql_timezone_tests() { + var e = getEngine(testCompilerOptions()); + // TODO: It'd be interesting to be able to inspect the + // possible set of expressions from the CQL engine API + // prior to evaluating them all + + var result = e.evaluate(toElmIdentifier("CqlTimeZoneTestSuite"), evalTime); + + for (var entry : result.expressionResults.entrySet()) { + if(entry.getKey().toString().startsWith("test")) { + if(((ExpressionResult)entry.getValue()).value() != null) { + Assert.assertEquals( + (String) ((ExpressionResult) entry.getValue()).value(), + entry.getKey().toString().replaceAll("test_", "") + " TEST PASSED" + ); + } + } + } + } + protected CqlCompilerOptions testCompilerOptions() { var options = CqlCompilerOptions.defaultOptions(); // This test suite contains some definitions that use features that are usually diff --git a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTestBase.java b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTestBase.java index f16e105f2..c697ca570 100644 --- a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTestBase.java +++ b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTestBase.java @@ -5,8 +5,24 @@ import org.cqframework.cql.cql2elm.*; import org.hl7.elm.r1.Library; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; public class CqlTestBase { + static final String NORTH_AMERICA_MOUNTAIN = "America/Denver"; // This is the baseline: Normal hour on the hour timezone + static final String NEWFOUNDLAND = "America/St_Johns"; + static final String INDIA = "Asia/Kolkata"; + static final String AUSTRALIA_NORTHERN_TERRITORY = "Australia/Darwin"; + static final String AUSTRALIA_EUCLA= "Australia/Eucla"; + static final String AUSTRALIA_BROKEN_HILL = "Australia/Broken_Hill"; + static final String AUSTRALIA_LORD_HOWE = "Australia/Lord_Howe"; + static final String AUSTRALIA_SOUTH = "Australia/Adelaide"; + static final String INDIAN_COCOS = "Indian/Cocos"; + static final String PACIFIC_CHATHAM = "Pacific/Chatham"; + + @DataProvider + static Object[][] timezones() { + return new Object[][] {{NORTH_AMERICA_MOUNTAIN},{NEWFOUNDLAND},{INDIA},{AUSTRALIA_NORTHERN_TERRITORY},{AUSTRALIA_EUCLA},{AUSTRALIA_BROKEN_HILL},{AUSTRALIA_LORD_HOWE},{AUSTRALIA_SOUTH},{INDIAN_COCOS},{PACIFIC_CHATHAM}}; + } private static ModelManager modelManager; protected static ModelManager getModelManager() { diff --git a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTimezoneTests.java b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTimezoneTests.java new file mode 100644 index 000000000..d947c833c --- /dev/null +++ b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTimezoneTests.java @@ -0,0 +1,42 @@ +package org.opencds.cqf.cql.engine.execution; + +import org.hl7.elm.r1.VersionedIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.*; +import org.testng.asserts.SoftAssert; + +import java.util.TimeZone; + +@SuppressWarnings("removal") +public class CqlTimezoneTests extends CqlTestBase { + private static final Logger logger = LoggerFactory.getLogger(CqlTimezoneTests.class); + + private static final VersionedIdentifier library = new VersionedIdentifier().withId("CqlTimezoneTests"); + + @Test(dataProvider = "timezones") + public void testExpressionsProblematicForWeirdTimezones(String timezone) { + final String oldTz = System.getProperty("user.timezone"); + // This is the ONLY thing that will work. System.setProperty() and -Duser.timezone do NOT work + TimeZone.setDefault(TimeZone.getTimeZone(timezone)); + + try { + final SoftAssert softAssert = new SoftAssert(); + + evaluateExpression("After_SameHour", false, softAssert); + evaluateExpression("SameAs_SameHour", true, softAssert); + evaluateExpression("SameOrAfter_HourBefore", false, softAssert); + evaluateExpression("SameOrBefore_SameHour", true, softAssert); + + softAssert.assertAll(); + } finally { + TimeZone.setDefault(TimeZone.getTimeZone(oldTz)); + } + } + + private void evaluateExpression(String functionName, boolean expectedResult, SoftAssert softAssert) { + Object result = engine.expression(library, functionName).value(); + softAssert.assertEquals(result, expectedResult, functionName); + } + +} diff --git a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTypesOperatorsTest.java b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTypesOperatorsTest.java index 27faf2a6b..a838f29ed 100644 --- a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTypesOperatorsTest.java +++ b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/execution/CqlTypesOperatorsTest.java @@ -1,289 +1,284 @@ package org.opencds.cqf.cql.engine.execution; import org.opencds.cqf.cql.engine.runtime.*; -import org.testng.Assert; import org.testng.annotations.Test; import org.opencds.cqf.cql.engine.elm.executing.EquivalentEvaluator; +import org.testng.asserts.SoftAssert; import java.math.BigDecimal; import java.util.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - public class CqlTypesOperatorsTest extends CqlTestBase { + @Test(dataProvider = "timezones") + public void test_all_types_operators(String timezone) { + final String oldTz = System.getProperty("user.timezone"); + // This is the ONLY thing that will work. System.setProperty() and -Duser.timezone do NOT work + TimeZone.setDefault(TimeZone.getTimeZone(timezone)); - @Test - public void test_all_types_operators() { - - Set set = new HashSet<>(); - EvaluationResult evaluationResult; - - evaluationResult = engine.evaluate(toElmIdentifier("CqlTypeOperatorsTest")); - Object result; - - result = evaluationResult.forExpression("AsQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("45.5")).withUnit("g"))); + try { + final SoftAssert softAssert = new SoftAssert(); - result = evaluationResult.forExpression("CastAsQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("45.5")).withUnit("g"))); + EvaluationResult evaluationResult; - result = evaluationResult.forExpression("AsDateTime").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1, 1))); + evaluationResult = engine.evaluate(toElmIdentifier("CqlTypeOperatorsTest")); + Object result; - result = evaluationResult.forExpression("IntegerToDecimal").value(); - assertThat(result, is(new BigDecimal(5))); + result = evaluationResult.forExpression("AsQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("45.5")).withUnit("g"))); - result = evaluationResult.forExpression("IntegerToString").value(); - assertThat(result, is("5")); + result = evaluationResult.forExpression("CastAsQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("45.5")).withUnit("g"))); + result = evaluationResult.forExpression("AsDateTime").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1, 1))); - result = evaluationResult.forExpression("StringToDateTime").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1, 1))); + result = evaluationResult.forExpression("IntegerToDecimal").value(); + softAssert.assertEquals(result, new BigDecimal(5)); - result = evaluationResult.forExpression("StringToTime").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new Time(14, 30, 0, 0))); + result = evaluationResult.forExpression("IntegerToString").value(); + softAssert.assertEquals(result, "5"); - result = evaluationResult.forExpression("ConvertQuantity").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new Quantity().withValue(new BigDecimal("0.005")).withUnit("g"))); + result = evaluationResult.forExpression("StringToDateTime").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1, 1))); - result = evaluationResult.forExpression("ConvertSyntax").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new Quantity().withValue(new BigDecimal("0.005")).withUnit("g"))); + result = evaluationResult.forExpression("StringToTime").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new Time(14, 30, 0, 0))); - result = evaluationResult.forExpression("ConvertsToBooleanTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertQuantity").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new Quantity().withValue(new BigDecimal("0.005")).withUnit("g"))); - result = evaluationResult.forExpression("ConvertsToBooleanFalse").value(); - Assert.assertFalse((Boolean) result); + result = evaluationResult.forExpression("ConvertSyntax").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new Quantity().withValue(new BigDecimal("0.005")).withUnit("g"))); - result = evaluationResult.forExpression("ConvertsToBooleanNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("ConvertsToBooleanTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToDateTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToBooleanFalse").value(); + softAssert.assertFalse((Boolean) result); - result = evaluationResult.forExpression("ConvertsToDateFalse").value(); - Assert.assertFalse((Boolean) result); + result = evaluationResult.forExpression("ConvertsToBooleanNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("ConvertsToDateNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("ConvertsToDateTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToDateTimeStringTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToDateFalse").value(); + softAssert.assertFalse((Boolean) result); - result = evaluationResult.forExpression("ConvertsToDateTimeDateTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToDateNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("ConvertsToDateTimeFalse").value(); - Assert.assertFalse((Boolean) result); + result = evaluationResult.forExpression("ConvertsToDateTimeStringTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToDateTimeNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("ConvertsToDateTimeDateTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToDecimalTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToDateTimeFalse").value(); + softAssert.assertFalse((Boolean) result); - result = evaluationResult.forExpression("ConvertsToDecimalFalse").value(); - Assert.assertFalse((Boolean) result); + result = evaluationResult.forExpression("ConvertsToDateTimeNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("ConvertsToDecimalNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("ConvertsToDecimalTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToIntegerTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToDecimalFalse").value(); + softAssert.assertFalse((Boolean) result); - result = evaluationResult.forExpression("ConvertsToIntegerLong").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToDecimalNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("ConvertsToIntegerFalse").value(); - Assert.assertFalse((Boolean) result); + result = evaluationResult.forExpression("ConvertsToIntegerTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToIntegerNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("ConvertsToIntegerLong").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToLongTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToIntegerFalse").value(); + softAssert.assertFalse((Boolean) result); - result = evaluationResult.forExpression("ConvertsToLongFalse").value(); - Assert.assertFalse((Boolean) result); + result = evaluationResult.forExpression("ConvertsToIntegerNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("ConvertsToLongNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("ConvertsToLongTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToQuantityStringTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToLongFalse").value(); + softAssert.assertFalse((Boolean) result); - result = evaluationResult.forExpression("ConvertsToQuantityStringFalse").value(); - Assert.assertFalse((Boolean) result); + result = evaluationResult.forExpression("ConvertsToLongNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("ConvertsToQuantityIntegerTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToQuantityStringTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToQuantityDecimalTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToQuantityStringFalse").value(); + softAssert.assertFalse((Boolean) result); - result = evaluationResult.forExpression("ConvertsToQuantityRatioTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToQuantityIntegerTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToQuantityNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("ConvertsToQuantityDecimalTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToStringBoolean").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToQuantityRatioTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToStringInteger").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToQuantityNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("ConvertsToStringLong").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToStringBoolean").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToStringDecimal").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToStringInteger").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToStringQuantity").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToStringLong").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToStringRatio").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToStringDecimal").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToStringDate").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToStringQuantity").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToStringDateTime").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToStringRatio").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToStringTime").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToStringDate").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToStringNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("ConvertsToStringDateTime").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToTimeTrue").value(); - Assert.assertTrue((Boolean) result); + result = evaluationResult.forExpression("ConvertsToStringTime").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("ConvertsToTimeFalse").value(); - Assert.assertFalse((Boolean) result); + result = evaluationResult.forExpression("ConvertsToStringNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("ConvertsToTimeNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("ConvertsToTimeTrue").value(); + softAssert.assertTrue((Boolean) result); - result = evaluationResult.forExpression("IntegerIsInteger").value(); - assertThat(result, is(true)); + result = evaluationResult.forExpression("ConvertsToTimeFalse").value(); + softAssert.assertFalse((Boolean) result); - result = evaluationResult.forExpression("StringIsInteger").value(); - assertThat(result, is(false)); + result = evaluationResult.forExpression("ConvertsToTimeNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("StringNoToBoolean").value(); - assertThat(result, is(false)); + result = evaluationResult.forExpression("IntegerIsInteger").value(); + softAssert.assertEquals(result, true); - result = evaluationResult.forExpression("CodeToConcept1").value(); - Assert.assertTrue(((Concept) result).equivalent(new Concept().withCode(new Code().withCode("8480-6")))); + result = evaluationResult.forExpression("StringIsInteger").value(); + softAssert.assertEquals(result, false); - result = evaluationResult.forExpression("ToDateTime0").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1))); + result = evaluationResult.forExpression("StringNoToBoolean").value(); + softAssert.assertEquals(result, false); - result = evaluationResult.forExpression("ToDateTime1").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1, 1))); - // assertThat(((DateTime)result).getTimezoneOffset(), is(new BigDecimal("-7"))); + result = evaluationResult.forExpression("CodeToConcept1").value(); + softAssert.assertTrue(((Concept) result).equivalent(new Concept().withCode(new Code().withCode("8480-6")))); - result = evaluationResult.forExpression("ToDateTime2").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1, 1, 12, 5))); - // assertThat(((DateTime)result).getTimezoneOffset(), is(new BigDecimal("-7"))); + result = evaluationResult.forExpression("ToDateTime0").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1))); - result = evaluationResult.forExpression("ToDateTime3").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1, 1, 12, 5, 5, 955))); - // assertThat(((DateTime)result).getTimezoneOffset(), is(new BigDecimal("-7"))); + result = evaluationResult.forExpression("ToDateTime1").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1, 1))); - result = evaluationResult.forExpression("ToDateTime4").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(new BigDecimal("1.5"), 2014, 1, 1, 12, 5, 5, 955))); - // assertThat(((DateTime)result).getTimezoneOffset(), is(new BigDecimal("1.5"))); + result = evaluationResult.forExpression("ToDateTime2").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1, 1, 12, 5))); - result = evaluationResult.forExpression("ToDateTime5").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(new BigDecimal("-1.25"), 2014, 1, 1, 12, 5, 5, 955))); - // assertThat(((DateTime)result).getTimezoneOffset(), is(new BigDecimal("-1.25"))); + result = evaluationResult.forExpression("ToDateTime3").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(null, 2014, 1, 1, 12, 5, 5, 955))); - result = evaluationResult.forExpression("ToDateTime6").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(new BigDecimal(0), 2014, 1, 1, 12, 5, 5, 955))); - // assertThat(((DateTime)result).getTimezoneOffset(), is(new BigDecimal("-7"))); + result = evaluationResult.forExpression("ToDateTime4").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(new BigDecimal("1.5"), 2014, 1, 1, 12, 5, 5, 955)), "ToDateTime4 vs. new DateTime(-1.5)"); - result = evaluationResult.forExpression("ToDateTimeMalformed").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("ToDateTime5").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new DateTime(new BigDecimal("-1.25"), 2014, 1, 1, 12, 5, 5, 955)), "ToDateTime5 vs. new DateTime(-1.25)"); - result = evaluationResult.forExpression("String25D5ToDecimal").value(); - assertThat(result, is(new BigDecimal("25.5"))); + result = evaluationResult.forExpression("ToDateTime6").value(); - result = evaluationResult.forExpression("StringNeg25ToInteger").value(); - assertThat(result, is(-25)); + result = evaluationResult.forExpression("ToDateTimeMalformed").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("String123ToLong").value(); - assertThat(result, is(123L)); + result = evaluationResult.forExpression("String25D5ToDecimal").value(); + softAssert.assertEquals(result, new BigDecimal("25.5")); - result = evaluationResult.forExpression("String5D5CMToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("5.5")).withUnit("cm"))); + result = evaluationResult.forExpression("StringNeg25ToInteger").value(); + softAssert.assertEquals(result, -25); - result = evaluationResult.forExpression("StringInvalidToQuantityNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("String123ToLong").value(); + softAssert.assertEquals(result, 123L); - result = evaluationResult.forExpression("String100PerMinPerSqMeterToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("100")).withUnit("daL/min/m2"))); + result = evaluationResult.forExpression("String5D5CMToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("5.5")).withUnit("cm"))); - result = evaluationResult.forExpression("String100UnitPer10BillionToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("100")).withUnit("U/10*10{cells}"))); + result = evaluationResult.forExpression("StringInvalidToQuantityNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("String60DayPer7DayToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("60")).withUnit("d/(7.d)"))); + result = evaluationResult.forExpression("String100PerMinPerSqMeterToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("100")).withUnit("daL/min/m2"))); - result = evaluationResult.forExpression("String60EhrlichPer100gmToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("60")).withUnit("{EhrlichU}/100.g"))); + result = evaluationResult.forExpression("String100UnitPer10BillionToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("100")).withUnit("U/10*10{cells}"))); - result = evaluationResult.forExpression("StringPercentToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("60")).withUnit("%"))); + result = evaluationResult.forExpression("String60DayPer7DayToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("60")).withUnit("d/(7.d)"))); - result = evaluationResult.forExpression("StringPercentWithoutQuoteToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("70")).withUnit("%"))); + result = evaluationResult.forExpression("String60EhrlichPer100gmToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("60")).withUnit("{EhrlichU}/100.g"))); - result = evaluationResult.forExpression("StringPercentWithTabToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("80")).withUnit("%"))); + result = evaluationResult.forExpression("StringPercentToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("60")).withUnit("%"))); - result = evaluationResult.forExpression("StringPercentWithMultiSpacesToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("90")).withUnit("%"))); + result = evaluationResult.forExpression("StringPercentWithoutQuoteToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("70")).withUnit("%"))); - result = evaluationResult.forExpression("StringPercentWithSpacesUnitToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("10")).withUnit("ml"))); + result = evaluationResult.forExpression("StringPercentWithTabToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("80")).withUnit("%"))); - result = evaluationResult.forExpression("StringPercentWithQuoteUnitToQuantity").value(); - Assert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("20")).withUnit("ml"))); + result = evaluationResult.forExpression("StringPercentWithMultiSpacesToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("90")).withUnit("%"))); - result = evaluationResult.forExpression("ToRatioIsValid").value(); - Assert.assertTrue(((Ratio) result).getNumerator().equal(new Quantity().withValue(new BigDecimal("1.0")).withUnit("mg"))); - Assert.assertTrue(((Ratio) result).getDenominator().equal(new Quantity().withValue(new BigDecimal("2.0")).withUnit("mg"))); + result = evaluationResult.forExpression("StringPercentWithSpacesUnitToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("10")).withUnit("ml"))); - result = evaluationResult.forExpression("ToRatioIsNull").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("StringPercentWithQuoteUnitToQuantity").value(); + softAssert.assertTrue(((Quantity) result).equal(new Quantity().withValue(new BigDecimal("20")).withUnit("ml"))); - result = evaluationResult.forExpression("IntegerNeg5ToString").value(); - assertThat(result, is("-5")); + result = evaluationResult.forExpression("ToRatioIsValid").value(); + softAssert.assertTrue(((Ratio) result).getNumerator().equal(new Quantity().withValue(new BigDecimal("1.0")).withUnit("mg"))); + softAssert.assertTrue(((Ratio) result).getDenominator().equal(new Quantity().withValue(new BigDecimal("2.0")).withUnit("mg"))); - result = evaluationResult.forExpression("LongNeg5ToString").value(); - assertThat(result, is("-5")); + result = evaluationResult.forExpression("ToRatioIsNull").value(); + softAssert.assertNull(result); - result = evaluationResult.forExpression("Decimal18D55ToString").value(); - assertThat(result, is("18.55")); + result = evaluationResult.forExpression("IntegerNeg5ToString").value(); + softAssert.assertEquals(result, "-5"); - result = evaluationResult.forExpression("Quantity5D5CMToString").value(); - assertThat(result, is("5.5 'cm'")); + result = evaluationResult.forExpression("LongNeg5ToString").value(); + softAssert.assertEquals(result, "-5"); - result = evaluationResult.forExpression("BooleanTrueToString").value(); - assertThat(result, is("true")); + result = evaluationResult.forExpression("Decimal18D55ToString").value(); + softAssert.assertEquals(result, "18.55"); - result = evaluationResult.forExpression("ToTime1").value(); - Assert.assertTrue(EquivalentEvaluator.equivalent(result, new Time(14, 30, 0, 0))); + result = evaluationResult.forExpression("Quantity5D5CMToString").value(); + softAssert.assertEquals(result, "5.5 'cm'"); - result = evaluationResult.forExpression("ToTimeMalformed").value(); - Assert.assertNull(result); + result = evaluationResult.forExpression("BooleanTrueToString").value(); + softAssert.assertEquals(result, "true"); + result = evaluationResult.forExpression("ToTime1").value(); + softAssert.assertTrue(EquivalentEvaluator.equivalent(result, new Time(14, 30, 0, 0))); + result = evaluationResult.forExpression("ToTimeMalformed").value(); + softAssert.assertNull(result); + softAssert.assertAll(); + } finally { + TimeZone.setDefault(TimeZone.getTimeZone(oldTz)); + } } } diff --git a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/runtime/DateTimeTest.java b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/runtime/DateTimeTest.java new file mode 100644 index 000000000..b5fd66025 --- /dev/null +++ b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/runtime/DateTimeTest.java @@ -0,0 +1,201 @@ +package org.opencds.cqf.cql.engine.runtime; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.util.List; + +import static org.testng.Assert.*; + +public class DateTimeTest { + private static final LocalDateTime DST_2023_10_26_22_12_0 = LocalDateTime.of(2023, Month.OCTOBER, 26, 22, 12, 0); + private static final LocalDateTime DST_2023_11_03_02_52_0 = LocalDateTime.of(2023, Month.NOVEMBER, 3, 2, 52, 0); + // This is OUTSIDE of Daylight Savings Time + private static final LocalDateTime NON_DST_2024_02_27_07_28_0 = LocalDateTime.of(2024, Month.FEBRUARY, 27, 7, 28, 0); + private static final LocalDateTime DST_2024_06_15_23_32_0 = LocalDateTime.of(2024, Month.JULY, 15, 23, 32, 0); + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + + private static final String DST_2023_10_26_22_12_0_STRING = FORMATTER.format(DST_2023_10_26_22_12_0); + private static final String DST_2023_11_03_02_52_0_STRING = FORMATTER.format(DST_2023_11_03_02_52_0); + private static final String NON_DST_2024_02_27_07_28_0_STRING = FORMATTER.format(NON_DST_2024_02_27_07_28_0); + private static final String DST_2024_06_15_23_32_0_STRING = FORMATTER.format(DST_2024_06_15_23_32_0); + + private static final List DST_2023_10_26_22_12_0_INTS = toList(DST_2024_06_15_23_32_0); + + private static final List DST_2023_11_03_02_52_0_INTS = toList(DST_2023_11_03_02_52_0); + private static final List NON_DST_2024_02_27_07_28_0_INTS = toList(NON_DST_2024_02_27_07_28_0); + private static final List DST_2024_06_15_23_32_0_INTS = toList(DST_2024_06_15_23_32_0); + + private static final ZoneOffset DST_OFFSET_NORTH_AMERICA_EASTERN = ZoneOffset.of("-04:00"); + private static final ZoneOffset NON_DST_OFFSET_NORTH_AMERICA_EASTERN = ZoneOffset.of("-05:00"); + private static final ZoneOffset DST_OFFSET_NORTH_AMERICA_MOUNTAIN = ZoneOffset.of("-06:00"); + private static final ZoneOffset NON_DST_OFFSET_NORTH_AMERICA_MOUNTAIN = ZoneOffset.of("-07:00"); + private static final ZoneOffset DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND = ZoneOffset.of("-02:30"); + // This offset doesn't exist for an ZoneID + private static final ZoneOffset NON_DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND = ZoneOffset.of("-03:30"); + + private static final BigDecimal DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_EASTERN = toBigDecimal(DST_OFFSET_NORTH_AMERICA_EASTERN); + private static final BigDecimal NON_DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_EASTERN = toBigDecimal(NON_DST_OFFSET_NORTH_AMERICA_EASTERN); + private static final BigDecimal DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_MOUNTAIN = toBigDecimal(DST_OFFSET_NORTH_AMERICA_MOUNTAIN); + private static final BigDecimal NON_DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_MOUNTAIN = toBigDecimal(NON_DST_OFFSET_NORTH_AMERICA_MOUNTAIN); + private static final BigDecimal DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_NEWFOUNDLAND = toBigDecimal(DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND); + private static final BigDecimal NON_DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_NEWFOUNDLAND = toBigDecimal(NON_DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND); + + private static BigDecimal toBigDecimal(ZoneOffset zoneOffset) { + final long offsetSeconds = zoneOffset.getLong(ChronoField.OFFSET_SECONDS); + final BigDecimal offsetMinutes = BigDecimal.valueOf(offsetSeconds) + .divide(BigDecimal.valueOf(60), 2, RoundingMode.CEILING); + return offsetMinutes .divide(BigDecimal.valueOf(60), 2, RoundingMode.CEILING); + } + + private static List toList(LocalDateTime localDateTime) { + return List.of(localDateTime.getYear(), localDateTime.getMonthValue(), localDateTime.getDayOfMonth(), localDateTime.getHour(), localDateTime.getMinute(), localDateTime.getSecond()); + } + + @DataProvider + private static Object[][] dateStrings() { + return new Object[][] { + {DST_2023_10_26_22_12_0_STRING, null, Precision.HOUR}, + {DST_2023_10_26_22_12_0_STRING, ZoneOffset.UTC, Precision.HOUR}, + {DST_2023_10_26_22_12_0_STRING, DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.HOUR}, + {DST_2023_10_26_22_12_0_STRING, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR}, + {DST_2023_10_26_22_12_0_STRING, DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.HOUR}, + {DST_2023_10_26_22_12_0_STRING, null, Precision.MILLISECOND}, + {DST_2023_11_03_02_52_0_STRING, ZoneOffset.UTC, Precision.MILLISECOND}, + {DST_2023_11_03_02_52_0_STRING, DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.MILLISECOND}, + {DST_2023_11_03_02_52_0_STRING, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.MILLISECOND}, + {DST_2023_11_03_02_52_0_STRING, DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.MILLISECOND}, + {NON_DST_2024_02_27_07_28_0_STRING, null, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0_STRING, ZoneOffset.UTC, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0_STRING, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0_STRING, NON_DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0_STRING, NON_DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.HOUR}, + {DST_2024_06_15_23_32_0_STRING, null, Precision.MILLISECOND}, + {DST_2024_06_15_23_32_0_STRING, ZoneOffset.UTC, Precision.MILLISECOND}, + {DST_2024_06_15_23_32_0_STRING, DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.MILLISECOND}, + {DST_2024_06_15_23_32_0_STRING, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.MILLISECOND}, + {DST_2024_06_15_23_32_0_STRING, DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.MILLISECOND} + }; + } + + @Test(dataProvider = "dateStrings") + void testDateStrings(String dateString, ZoneOffset zoneOffset, Precision precision) { + final DateTime dateTime = new DateTime(dateString, zoneOffset); + + final OffsetDateTime normalizedDateTime = dateTime.getNormalized(precision); + + assertEquals(normalizedDateTime, dateTime.getDateTime()); + } + + @DataProvider + private static Object[][] dateStringsOtherZoneId() { + return new Object[][]{ + {DST_2023_10_26_22_12_0, DST_OFFSET_NORTH_AMERICA_EASTERN, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR}, + {DST_2023_10_26_22_12_0, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR}, + {DST_2023_10_26_22_12_0, DST_OFFSET_NORTH_AMERICA_EASTERN, DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.HOUR}, + {DST_2023_10_26_22_12_0, DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, NON_DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_MOUNTAIN, NON_DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, NON_DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.HOUR}, + {DST_2023_10_26_22_12_0, DST_OFFSET_NORTH_AMERICA_EASTERN, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.MILLISECOND}, + {DST_2023_10_26_22_12_0, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.MILLISECOND}, + {DST_2023_10_26_22_12_0, DST_OFFSET_NORTH_AMERICA_EASTERN, DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.MILLISECOND}, + {DST_2023_10_26_22_12_0, DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.MILLISECOND}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, NON_DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.MILLISECOND}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_MOUNTAIN, NON_DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.MILLISECOND}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, NON_DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.MILLISECOND}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.MILLISECOND}, + }; + } + + @Test(dataProvider = "dateStringsOtherZoneId") + void testDateStringsOtherZoneId(LocalDateTime localDateTime, ZoneOffset zoneOffsetInit, ZoneOffset zonedOffsetGetNormalized, Precision precision) { + final OffsetDateTime expectedOffsetDateTime = OffsetDateTime.of(localDateTime.minusSeconds(zoneOffsetInit.getTotalSeconds() - zonedOffsetGetNormalized.getTotalSeconds()), zonedOffsetGetNormalized); + final DateTime dateTime = new DateTime(FORMATTER.format(localDateTime), zoneOffsetInit); + + final OffsetDateTime normalizedDateTime = dateTime.getNormalized(precision, zonedOffsetGetNormalized); + + assertEquals(normalizedDateTime, expectedOffsetDateTime); + } + + @DataProvider + private static Object[][] offsetPrecisions() { + return new Object[][] { + {DST_2023_10_26_22_12_0, ZoneOffset.UTC, Precision.HOUR}, + {DST_2023_10_26_22_12_0, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.HOUR}, + {DST_2023_10_26_22_12_0, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR}, + {DST_2023_10_26_22_12_0, DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.HOUR}, + {DST_2023_11_03_02_52_0, ZoneOffset.UTC, Precision.MILLISECOND}, + {DST_2023_11_03_02_52_0, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.MILLISECOND}, + {DST_2023_11_03_02_52_0, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.MILLISECOND}, + {DST_2023_11_03_02_52_0, DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.MILLISECOND}, + {NON_DST_2024_02_27_07_28_0, ZoneOffset.UTC, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR}, + {NON_DST_2024_02_27_07_28_0, NON_DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.HOUR}, + {DST_2024_06_15_23_32_0, ZoneOffset.UTC, Precision.MILLISECOND}, + {DST_2024_06_15_23_32_0, NON_DST_OFFSET_NORTH_AMERICA_EASTERN, Precision.MILLISECOND}, + {DST_2024_06_15_23_32_0, DST_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.MILLISECOND}, + {DST_2024_06_15_23_32_0, DST_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.MILLISECOND}, + }; + } + + @Test(dataProvider = "offsetPrecisions") + void testOffsetPrecisions(LocalDateTime localDateTime, ZoneOffset zoneOffset, Precision precision) { + final OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, zoneOffset); + final DateTime dateTimeNoPrecision = new DateTime(offsetDateTime); + final DateTime dateTimePrecision = new DateTime(offsetDateTime, precision); + + final OffsetDateTime normalizedDateTimeNoPrecision = dateTimeNoPrecision.getNormalized(precision); + final OffsetDateTime normalizedDateTimePrecision = dateTimePrecision.getNormalized(precision); + + assertEquals(normalizedDateTimeNoPrecision, dateTimeNoPrecision.getDateTime()); + assertEquals(normalizedDateTimePrecision, dateTimePrecision.getDateTime()); + } + + @DataProvider + private static Object[][] bigDecimals() { + return new Object[][] { + {null, Precision.HOUR, DST_2023_10_26_22_12_0_INTS}, + {BigDecimal.ZERO, Precision.HOUR, DST_2023_10_26_22_12_0_INTS}, + {DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_EASTERN, Precision.HOUR, DST_2023_10_26_22_12_0_INTS}, + {DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR, DST_2023_10_26_22_12_0_INTS}, + {DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.HOUR, DST_2023_10_26_22_12_0_INTS}, + {null, Precision.MILLISECOND, DST_2023_11_03_02_52_0_INTS}, + {BigDecimal.ZERO, Precision.MILLISECOND, DST_2023_11_03_02_52_0_INTS}, + {DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_EASTERN, Precision.MILLISECOND, DST_2023_11_03_02_52_0_INTS}, + {DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.MILLISECOND, DST_2023_11_03_02_52_0_INTS}, + {DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.MILLISECOND, DST_2023_11_03_02_52_0_INTS}, + {null, Precision.HOUR, NON_DST_2024_02_27_07_28_0_INTS}, + {BigDecimal.ZERO, Precision.HOUR, NON_DST_2024_02_27_07_28_0_INTS}, + {NON_DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_EASTERN, Precision.HOUR, NON_DST_2024_02_27_07_28_0_INTS}, + {NON_DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR, NON_DST_2024_02_27_07_28_0_INTS}, + {NON_DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.HOUR, NON_DST_2024_02_27_07_28_0_INTS}, + {null, Precision.MILLISECOND, DST_2024_06_15_23_32_0_INTS}, + {BigDecimal.ZERO, Precision.MILLISECOND, DST_2024_06_15_23_32_0_INTS}, + {DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_EASTERN, Precision.MILLISECOND, DST_2024_06_15_23_32_0_INTS}, + {DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_MOUNTAIN, Precision.HOUR, DST_2024_06_15_23_32_0_INTS}, + {DST_BIG_DECIMAL_OFFSET_NORTH_AMERICA_NEWFOUNDLAND, Precision.MILLISECOND, DST_2024_06_15_23_32_0_INTS}, + }; + } + + @Test(dataProvider = "bigDecimals") + void testBigDecimal(BigDecimal offset, Precision precision, List dateElements) { + final int[] dateElementsArray = dateElements.stream().mapToInt(anInt -> anInt).toArray(); + final DateTime dateTime = new DateTime(offset, dateElementsArray); + + final OffsetDateTime normalizedDateTime = dateTime.getNormalized(precision); + + assertEquals(normalizedDateTime, dateTime.getDateTime()); + } +} \ No newline at end of file diff --git a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTimeZoneTestSuite.cql b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTimeZoneTestSuite.cql new file mode 100644 index 000000000..a73458ce1 --- /dev/null +++ b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTimeZoneTestSuite.cql @@ -0,0 +1,172 @@ +library CqlTimeZoneTestSuite version '2' + +define function TestMessage(condition Boolean, testName String, expected String, result String): + if condition + then testName + ' TEST PASSED' + else Message('TEST SUITE FAILED', true, 'Test ' + testName + ' failed', 'Warning', 'Expected ' + expected + ' Found ' + result) + +define function toString(value Boolean): + if value is null + then 'null' + else ToString(value) + +define function toString(value Decimal): + if value is null + then 'null' + else ToString(value) + +define function toString(value Integer): + if value is null + then 'null' + else ToString(value) + +define function toString(value DateTime): + if value is null + then 'null' + else ToString(value) + +define function toString(value Date): + if value is null + then 'null' + else ToString(value) + +define function toString(value Time): + if value is null + then 'null' + else ToString(value) + +define function toString(value Quantity): + if value is null + then 'null' + else ToString(value) + +define function toString(value Code): + if value is null + then 'null' + else 'Code { code: ' + value.code + ' system: ' + value.system + ' version: ' + value.version + ' display: ' + value.display + ' }' + +define function toString(value List): + if value is null + then 'null' + else Combine((value V return toString(V)), ', ') + +define function toString(value Concept): + if value is null + then 'null' + else 'Concept { ' + toString(value.codes) + ' display: ' + value.display + ' }' + +define function toString(value Tuple{a Integer}): + if value is null + then 'null' + else 'Tuple{ a: ' + ToString(value.a) + ' }' + +define function toString(value Tuple{hello String}): + if value is null + then 'null' + else 'Tuple{ hello: ' + value.hello + ' }' + +define function toString(value Tuple{a Integer, b String}): + if value is null + then 'null' + else 'Tuple{ a: ' + ToString(value.a) + ', b: ' + value.b + ' }' + +define function toString(value Tuple{a Integer, b Integer, c Integer}): + if value is null + then 'null' + else 'Tuple{ a: ' + ToString(value.a) + ', b: ' + ToString(value.b) + ', c: ' + ToString(value.c) + ' }' + +define function toString(value List): + if value is null + then 'null' + else Combine((value V return toString(V)), ', ') + +define function toString(value Interval): + if value is null + then 'null' + else '[' + toString(start of value) + ', ' + toString(end of value) + ']' + +define function toString(value Interval): + if value is null + then 'null' + else '[' + toString(start of value) + ', ' + toString(end of value) + ']' + +define function toString(value Interval): + if value is null + then 'null' + else '[' + toString(start of value) + ', ' + toString(end of value) + ']' + +define function toString(value Interval): + if value is null + then 'null' + else '[' + toString(start of value) + ', ' + toString(end of value) + ']' + +define function toString(value Interval): + if value is null + then 'null' + else '[' + toString(start of value) + ', ' + toString(end of value) + ']' + +define function toString(value Interval