diff --git a/src/test/java/org/javarosa/core/model/data/test/TimeDataLimitationsTest.java b/src/test/java/org/javarosa/core/model/data/test/TimeDataLimitationsTest.java index 2bd0b14ac..9b4b51969 100644 --- a/src/test/java/org/javarosa/core/model/data/test/TimeDataLimitationsTest.java +++ b/src/test/java/org/javarosa/core/model/data/test/TimeDataLimitationsTest.java @@ -1,63 +1,54 @@ package org.javarosa.core.model.data.test; +import static org.javarosa.test.utils.SystemHelper.withTimeZone; +import static org.junit.Assert.assertEquals; + +import java.util.Date; +import java.util.TimeZone; import org.javarosa.core.model.data.TimeData; import org.javarosa.core.model.utils.DateUtils; -import org.junit.After; -import org.junit.Before; import org.junit.Test; -import java.util.Date; -import java.util.TimeZone; - -import static org.junit.Assert.assertEquals; /** - This test is intended to show the limitation of {@link TimeData}. - - Using this data type in countries which change their time (summer/winter - DST) will cause that forms saved during - wintertime and then edited during summertime (and vice versa) will be treated as saved in a neighbor's timezone. - It's because we have just time and time offset like: 10:00:00.000+02:00 but we don't know when the form has been - saved so we parse it using the current date. - - Example: - If we saved 10:00:00.000+02:00 during summertime (in Poland) and we are editing the form during winter time our - timezone is +01:00 not +02:00. As mentioned above javarosa doesn't know that the form has been saved in the same location - but different timezone because of DST so it treats the value like saved in the neighbor timezone - (in Kiev or London for example). - - Related issues: - https://github.com/opendatakit/javarosa/pull/478 - https://github.com/opendatakit/collect/issues/170 + * This test is intended to show the limitation of {@link TimeData}. + *

+ * Using this data type in countries which change their time (summer/winter - DST) will cause that forms saved during + * wintertime and then edited during summertime (and vice versa) will be treated as saved in a neighbor's timezone. + * It's because we have just time and time offset like: 10:00:00.000+02:00 but we don't know when the form has been + * saved so we parse it using the current date. + *

+ * Example: + * If we saved 10:00:00.000+02:00 during summertime (in Poland) and we are editing the form during winter time our + * timezone is +01:00 not +02:00. As mentioned above javarosa doesn't know that the form has been saved in the same location + * but different timezone because of DST so it treats the value like saved in the neighbor timezone + * (in Kiev or London for example). + *

+ * Related issues: + * https://github.com/opendatakit/javarosa/pull/478 + * https://github.com/opendatakit/collect/issues/170 */ public class TimeDataLimitationsTest { - - private TimeZone backupTimeZone; - - @Before - public void setUp() { - backupTimeZone = TimeZone.getDefault(); - } - - @After - public void tearDown() { - TimeZone.setDefault(backupTimeZone); - } + public static final TimeZone WARSAW = TimeZone.getTimeZone("Europe/Warsaw"); + public static final TimeZone KIEV = TimeZone.getTimeZone("Europe/Kiev"); @Test public void editingFormsSavedInDifferentTimezoneTest() { + StringWrapper savedTime = StringWrapper.empty(); // A user is in Warsaw (GMT+2) saved a form with the time question - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Warsaw")); - - boolean isSummerTime = TimeZone.getDefault().inDaylightTime(new Date()); - String savedTime = isSummerTime ? "10:00:00.000+02:00" : "10:00:00.000+01:00"; - - // A user opens saved form in Warsaw as well - the hour should be the same - TimeData timeData = new TimeData(DateUtils.parseTime(savedTime)); - assertEquals("10:00", timeData.getDisplayText()); - + withTimeZone(WARSAW, () -> { + boolean isSummerTime = TimeZone.getDefault().inDaylightTime(new Date()); + savedTime.set(isSummerTime ? "10:00:00.000+02:00" : "10:00:00.000+01:00"); + + // A user opens saved form in Warsaw as well - the hour should be the same + TimeData timeData = new TimeData(DateUtils.parseTime(savedTime.get())); + assertEquals("10:00", timeData.getDisplayText()); + }); // A user travels to Kiev (GMT+3) and opens the saved form again - the hour should be edited +1h - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Kiev")); - timeData = new TimeData(DateUtils.parseTime(savedTime)); - assertEquals("11:00", timeData.getDisplayText()); + withTimeZone(KIEV, () -> { + TimeData timeData = new TimeData(DateUtils.parseTime(savedTime.get())); + assertEquals("11:00", timeData.getDisplayText()); + }); + } @Test @@ -67,17 +58,37 @@ public void editingFormsSavedInTheSameLocationButAfterDSTChangeTest() { dateFields.month = 8; dateFields.day = 1; - // A user is in Warsaw (during summer time - GMT+2) and saved a form with the time question - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Warsaw")); - String savedTime = "10:00:00.000+02:00"; + withTimeZone(WARSAW, () -> { + String savedTime = "10:00:00.000+02:00"; + + // A user opens saved form in Warsaw and during summertime as well - the hour should be the same + TimeData timeData = new TimeData(DateUtils.parseTimeWithFixedDate(savedTime, dateFields)); + assertEquals("10:00", timeData.getDisplayText()); + + // A user opens saved form in Warsaw as well but during wintertime - the hour is edited -1h (the mentioned limitation) + dateFields.month = 12; + timeData = new TimeData(DateUtils.parseTimeWithFixedDate(savedTime, dateFields)); + assertEquals("09:00", timeData.getDisplayText()); + }); + } + + static class StringWrapper { + private String value; + + StringWrapper(String value) { + this.value = value; + } + + static StringWrapper empty() { + return new StringWrapper(""); + } - // A user opens saved form in Warsaw and during summertime as well - the hour should be the same - TimeData timeData = new TimeData(DateUtils.parseTimeWithFixedDate(savedTime, dateFields)); - assertEquals("10:00", timeData.getDisplayText()); + public String get() { + return value; + } - // A user opens saved form in Warsaw as well but during wintertime - the hour is edited -1h (the mentioned limitation) - dateFields.month = 12; - timeData = new TimeData(DateUtils.parseTimeWithFixedDate(savedTime, dateFields)); - assertEquals("09:00", timeData.getDisplayText()); + public void set(String value) { + this.value = value; + } } } diff --git a/src/test/java/org/javarosa/core/model/utils/test/DateUtilsFormatLocalizationTests.java b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsFormatLocalizationTests.java new file mode 100644 index 000000000..e6cacc908 --- /dev/null +++ b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsFormatLocalizationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2009 JavaRosa + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.javarosa.core.model.utils.test; + +import static java.time.DayOfWeek.SUNDAY; +import static java.time.Month.JANUARY; +import static java.time.format.TextStyle.SHORT; +import static org.hamcrest.Matchers.is; +import static org.javarosa.test.utils.SystemHelper.withLocale; +import static org.junit.Assert.assertThat; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.Date; +import java.util.Locale; +import java.util.stream.Stream; +import org.javarosa.core.model.utils.DateUtils; +import org.junit.Test; + +public class DateUtilsFormatLocalizationTests { + @Test + public void format_is_localized() { + // Use a Sunday in January for our test + LocalDateTime localDateTime = LocalDateTime.parse("2018-01-07T10:20:30.400"); + Date date = Date.from(localDateTime.toInstant(OffsetDateTime.now().getOffset())); + + Stream.of( + Locale.ENGLISH, + Locale.forLanguageTag("es-ES"), + Locale.FRENCH + ).forEach(locale -> withLocale(locale, l -> { + assertThat(DateUtils.format(date, "%b"), is(JANUARY.getDisplayName(SHORT, l))); + assertThat(DateUtils.format(date, "%a"), is(SUNDAY.getDisplayName(SHORT, l))); + })); + } + +} diff --git a/src/test/java/org/javarosa/core/model/utils/test/DateUtilsFormatSanityCheckTests.java b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsFormatSanityCheckTests.java new file mode 100644 index 000000000..29d93b530 --- /dev/null +++ b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsFormatSanityCheckTests.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2009 JavaRosa + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.javarosa.core.model.utils.test; + +import static java.util.TimeZone.getTimeZone; +import static org.hamcrest.Matchers.is; +import static org.javarosa.core.model.utils.DateUtils.FORMAT_ISO8601; +import static org.javarosa.core.model.utils.DateUtils.formatDateTime; +import static org.javarosa.core.model.utils.DateUtils.parseDateTime; +import static org.javarosa.test.utils.SystemHelper.withTimeZone; +import static org.junit.Assert.assertThat; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.TimeZone; +import java.util.stream.Stream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class DateUtilsFormatSanityCheckTests { + @Parameterized.Parameter(value = 0) + public long inputTimestamp; + + @Parameterized.Parameters(name = "Input timestamp: {0}") + public static Collection data() { + return Arrays.asList(new Object[][]{ + {1300139579000L}, + {0} + }); + } + + @Test + public void sanity_check_iso_format_and_parse_back() { + Date input = new Date(inputTimestamp); + Stream.of( + TimeZone.getDefault(), + getTimeZone("UTC"), + getTimeZone("GMT+12"), + getTimeZone("GMT-13"), + getTimeZone("GMT+0230") + ).forEach(timeZone -> withTimeZone(timeZone, () -> + assertThat(parseDateTime(formatDateTime(input, FORMAT_ISO8601)), is(input)))); + } +} diff --git a/src/test/java/org/javarosa/core/model/utils/test/DateUtilsGetXmlStringValueTest.java b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsGetXmlStringValueTest.java new file mode 100644 index 000000000..baf1adbbd --- /dev/null +++ b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsGetXmlStringValueTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2009 JavaRosa + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.javarosa.core.model.utils.test; + +import static org.hamcrest.Matchers.is; +import static org.javarosa.core.model.utils.DateUtils.getXMLStringValue; +import static org.junit.Assert.assertThat; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.Date; +import org.junit.Test; + +public class DateUtilsGetXmlStringValueTest { + /** + * This test ensures that the Strings returned + * by the getXMLStringValue function are in + * the proper XML compliant format, which can be + * parsed by LocalDate.parse() + */ + @Test + public void xml_string_is_well_formatted() { + LocalDateTime nowDateTime = LocalDateTime.now(); + Date nowDate = Date.from(nowDateTime.toInstant(OffsetDateTime.now().getOffset())); + String nowXmlFormatterDate = getXMLStringValue(nowDate); + assertThat(LocalDate.parse(nowXmlFormatterDate), is(nowDateTime.toLocalDate())); + } + +} diff --git a/src/test/java/org/javarosa/core/model/utils/test/DateUtilsParseDateTimeTests.java b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsParseDateTimeTests.java new file mode 100644 index 000000000..b6bd0c179 --- /dev/null +++ b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsParseDateTimeTests.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2009 JavaRosa + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.javarosa.core.model.utils.test; + +import static java.util.TimeZone.getTimeZone; +import static org.hamcrest.Matchers.is; +import static org.javarosa.test.utils.SystemHelper.withTimeZone; +import static org.junit.Assert.assertThat; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.temporal.Temporal; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.TimeZone; +import java.util.stream.Stream; +import org.javarosa.core.model.utils.DateUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class DateUtilsParseDateTimeTests { + @Parameterized.Parameter(value = 0) + public String input; + + @Parameterized.Parameter(value = 1) + public Temporal expectedDateTime; + + @Parameterized.Parameters(name = "Input: {0}") + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"2016-04-13T16:26:00.000", LocalDateTime.parse("2016-04-13T16:26:00.000")}, + {"2016-04-13T16:26:00.000-07", OffsetDateTime.parse("2016-04-13T16:26:00.000-07:00")}, + {"2015-12-16T16:09:00.000-08", OffsetDateTime.parse("2015-12-16T16:09:00.000-08:00")}, + {"2015-12-16T07:09:00.000+08", OffsetDateTime.parse("2015-12-16T07:09:00.000+08:00")}, + {"2015-11-30T16:09:00.000-08", OffsetDateTime.parse("2015-11-30T16:09:00.000-08:00")}, + {"2015-11-01T07:09:00.000+08", OffsetDateTime.parse("2015-11-01T07:09:00.000+08:00")}, + {"2015-12-31T16:09:00.000-08", OffsetDateTime.parse("2015-12-31T16:09:00.000-08:00")}, + }); + } + + @Test + public void parseDateTime_produces_expected_results_in_all_time_zones() { + Stream.of( + TimeZone.getDefault(), + getTimeZone("UTC"), + getTimeZone("GMT+12"), + getTimeZone("GMT-13"), + getTimeZone("GMT+0230") + ).forEach(tz -> withTimeZone(tz, () -> assertThat(parseDateTime(input), is(expectedDateTime)))); + } + + /** + * Returns a LocalDateTime or an OffsetTimeTime obtained from the result of + * calling DateUtils.parseDateTime() with the provided input, ensuring that + * both represent the same instant. + */ + private Temporal parseDateTime(String input) { + Instant inputInstant = Objects.requireNonNull(DateUtils.parseDateTime(input)).toInstant(); + + String timePart = input.substring(11); + if (timePart.contains("+") || timePart.contains("-")) { + // The input declares some positive or negative time offset + int beginOfOffsetPart = timePart.contains("+") ? timePart.indexOf("+") : timePart.indexOf("-"); + String offsetPart = timePart.substring(beginOfOffsetPart); + // The input declares some positive or negative time offset + String offset = offsetPart.length() == 3 ? offsetPart + ":00" : offsetPart; + return OffsetDateTime.ofInstant(inputInstant, ZoneId.of(offset)); + } + + if (input.endsWith("Z")) + // The input time is at UTC + return OffsetDateTime.ofInstant(inputInstant, ZoneId.of("Z")); + + // No time offset declared. Return a LocalTime + return LocalDateTime.ofInstant(inputInstant, ZoneId.systemDefault()); + } +} diff --git a/src/test/java/org/javarosa/core/model/utils/test/DateUtilsParseTimeTests.java b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsParseTimeTests.java new file mode 100644 index 000000000..097dbf48f --- /dev/null +++ b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsParseTimeTests.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2009 JavaRosa + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.javarosa.core.model.utils.test; + +import static java.util.TimeZone.getTimeZone; +import static org.hamcrest.Matchers.is; +import static org.javarosa.test.utils.SystemHelper.withTimeZone; +import static org.junit.Assert.assertThat; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneId; +import java.time.temporal.Temporal; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.TimeZone; +import java.util.stream.Stream; +import org.javarosa.core.model.utils.DateUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class DateUtilsParseTimeTests { + @Parameterized.Parameter(value = 0) + public String input; + + @Parameterized.Parameter(value = 1) + public Temporal expectedTime; + + @Parameterized.Parameters(name = "Input: {0}") + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"14:00", LocalTime.parse("14:00")}, + {"14:00Z", OffsetTime.parse("14:00Z")}, + {"14:00+02", OffsetTime.parse("14:00+02:00")}, + {"14:00-02", OffsetTime.parse("14:00-02:00")}, + {"14:00+02:30", OffsetTime.parse("14:00+02:30")}, + {"14:00-02:30", OffsetTime.parse("14:00-02:30")}, + }); + } + + @Test + public void parseTime_produces_expected_results_in_all_time_zones() { + // The tricky part of DateUtils.parseTime is that we allow for input time + // values to include time offset declarations, which has issues at different + // levels: + // - Conceptually, time values with offsets don't make sense until they're + // paired with a date so, how can we reason about what "10:00+02:00" means, + // and what should be a valid expected output for that input value? + // - Next, DateUtils.parseTime() produces Date values, which are a date and a + // time in the system's default time zone. Then, which date would we have to + // expect? + // + // To solve these issues, DateUtils.parseTime() will use the system's current + // date as a base for its output value whenever that is and whichever time zone + // the system's at. + // + // By testing parseTime under different system default time zones we're trying + // to have the confidence that our resulting Date objects will always translate + // to the same time declaration from the input string (ignoring their date part, + // of course). + Stream.of( + TimeZone.getDefault(), + getTimeZone("UTC"), + getTimeZone("GMT+12"), + getTimeZone("GMT-13"), + getTimeZone("GMT+0230") + ).forEach(tz -> withTimeZone(tz, () -> assertThat(parseTime(input), is(expectedTime)))); + } + + /** + * Returns a LocalTime or a OffsetTime obtained from the result of + * calling DateUtils.parseTime() with the provided input. + *

+ * The interim OffsetDateTime value ensures that it represents the + * same instant as the Date from the call to DateUtils.parseTime(). + */ + public Temporal parseTime(String input) { + Instant inputInstant = Objects.requireNonNull(DateUtils.parseTime(input)).toInstant(); + + if (input.contains("+") || input.contains("-")) { + // The input declares some positive or negative time offset + int beginOfOffsetPart = input.contains("+") ? input.indexOf("+") : input.indexOf("-"); + String offsetPart = input.substring(beginOfOffsetPart); + // The input declares some positive or negative time offset + String offset = offsetPart.length() == 3 ? offsetPart + ":00" : offsetPart; + return OffsetDateTime.ofInstant(inputInstant, ZoneId.of(offset)).toOffsetTime(); + } + + if (input.endsWith("Z")) + // The input time is at UTC + return OffsetDateTime.ofInstant(inputInstant, ZoneId.of("Z")).toOffsetTime(); + + // No time offset declared. Return a LocalTime + return LocalDateTime.ofInstant(inputInstant, ZoneId.systemDefault()).toLocalTime(); + } +} diff --git a/src/test/java/org/javarosa/core/model/utils/test/DateUtilsSCTOTests.java b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsSCTOTests.java index f04620ac8..eae831008 100644 --- a/src/test/java/org/javarosa/core/model/utils/test/DateUtilsSCTOTests.java +++ b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsSCTOTests.java @@ -6,67 +6,50 @@ package org.javarosa.core.model.utils.test; +import static org.javarosa.test.utils.SystemHelper.withTimeZone; import static org.junit.Assert.assertEquals; import java.util.Calendar; import java.util.Date; -import java.util.Locale; import java.util.SimpleTimeZone; import java.util.TimeZone; import org.javarosa.core.model.utils.DateUtils; -import org.junit.After; -import org.junit.Before; import org.junit.Ignore; import org.junit.Test; public class DateUtilsSCTOTests { - private Locale backupLocale; - private TimeZone backupZone; - - @Before - public void setUp() { - backupLocale = Locale.getDefault(); - backupZone = TimeZone.getDefault(); - } - - @After - public void tearDown() { - TimeZone.setDefault(backupZone); - Locale.setDefault(backupLocale); - } - @Test public void testParseDateTime() { - TimeZone.setDefault(TimeZone.getTimeZone("GMT+02")); + withTimeZone(TimeZone.getTimeZone("GMT+02"), () -> { + Date date = DateUtils.parseDateTime("2014-10-05T00:03:05.244+03"); + String str = DateUtils.formatDateTime(date, DateUtils.FORMAT_ISO8601); - Date date = DateUtils.parseDateTime("2014-10-05T00:03:05.244+03"); - String str = DateUtils.formatDateTime(date, DateUtils.FORMAT_ISO8601); - - assertEquals("2014-10-04T23:03:05.244+02:00", str); + assertEquals("2014-10-04T23:03:05.244+02:00", str); + }); } @Test public void testParseDateTime_withDST() { - applyDST(); - - Date date = DateUtils.parseDateTime("2014-10-05T00:03:05.244+03"); - String str = DateUtils.formatDateTime(date, DateUtils.FORMAT_ISO8601); + withTimeZone(buildDstTimeZone(), () -> { + Date date = DateUtils.parseDateTime("2014-10-05T00:03:05.244+03"); + String str = DateUtils.formatDateTime(date, DateUtils.FORMAT_ISO8601); - assertEquals("2014-10-05T00:03:05.244+03:00", str); + assertEquals("2014-10-05T00:03:05.244+03:00", str); + }); } @Test public void testParseTime() { - TimeZone.setDefault(TimeZone.getTimeZone("GMT+02")); + withTimeZone(TimeZone.getTimeZone("GMT+02"), () -> { + String time = "12:03:05.011+03"; - String time = "12:03:05.011+03"; + Date date = DateUtils.parseTime(time); - Date date = DateUtils.parseTime(time); + String formatted = DateUtils.formatTime(date, DateUtils.FORMAT_ISO8601); - String formatted = DateUtils.formatTime(date, DateUtils.FORMAT_ISO8601); - - assertEquals("11:03:05.011+02:00", formatted); + assertEquals("11:03:05.011+02:00", formatted); + }); } @Test @@ -77,27 +60,25 @@ public void testParseTime() { // - We're effectively binding all times to the EPOCH date // (1970-01-01, UTC), which has no DST public void testParseTime_withDST() { - applyDST(); - - String time = "12:03:05.011+03"; + withTimeZone(buildDstTimeZone(), () -> { + String time = "12:03:05.011+03"; - Date date = DateUtils.parseTime(time); + Date date = DateUtils.parseTime(time); - String formatted = DateUtils.formatTime(date, DateUtils.FORMAT_ISO8601); + String formatted = DateUtils.formatTime(date, DateUtils.FORMAT_ISO8601); - assertEquals("12:03:05.011+03", formatted); + assertEquals("12:03:05.011+03", formatted); + }); } - private void applyDST() { - // this is a timezone that operates DST every day of the year! - SimpleTimeZone dstTimezone = new SimpleTimeZone( - 2 * 60 * 60 * 1000, - "Europe/Athens", - Calendar.JANUARY, 1, 0, - 0, SimpleTimeZone.UTC_TIME, - Calendar.DECEMBER, 31, 0, - 24 * 60 * 60 * 1000, SimpleTimeZone.UTC_TIME, - 60 * 60 * 1000); - TimeZone.setDefault(dstTimezone); + private SimpleTimeZone buildDstTimeZone() { + return new SimpleTimeZone( + 2 * 60 * 60 * 1000, + "Europe/Athens", + Calendar.JANUARY, 1, 0, + 0, SimpleTimeZone.UTC_TIME, + Calendar.DECEMBER, 31, 0, + 24 * 60 * 60 * 1000, SimpleTimeZone.UTC_TIME, + 60 * 60 * 1000); } } diff --git a/src/test/java/org/javarosa/core/model/utils/test/DateUtilsTests.java b/src/test/java/org/javarosa/core/model/utils/test/DateUtilsTests.java deleted file mode 100644 index 46a17809d..000000000 --- a/src/test/java/org/javarosa/core/model/utils/test/DateUtilsTests.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright (C) 2009 JavaRosa - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package org.javarosa.core.model.utils.test; - -import static org.junit.Assert.assertEquals; - -import java.text.DateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.Locale; -import java.util.SimpleTimeZone; -import java.util.TimeZone; -import org.javarosa.core.model.utils.DateUtils; -import org.javarosa.core.model.utils.DateUtils.DateFields; -import org.joda.time.LocalDateTime; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -public class DateUtilsTests { - - private Locale backupLocale; - private TimeZone backupZone; - private Date testDate; - private LocalDateTime testLocalDateTime; - - @Before - public void setUp() { - backupLocale = Locale.getDefault(); - backupZone = TimeZone.getDefault(); - testLocalDateTime = new LocalDateTime(2018, 1, 1, 10, 20, 30, 400); - testDate = testLocalDateTime.toDate(); - } - - @After - public void tearDown() { - TimeZone.setDefault(backupZone); - Locale.setDefault(backupLocale); - } - - /** - * This test ensures that the Strings returned - * by the getXMLStringValue function are in - * the proper XML compliant format. - */ - @Test - public void testGetXMLStringValueFormat() { - String currentDate = DateUtils.getXMLStringValue(testDate); - assertEquals("The date string was not of the proper length", currentDate.length(), "YYYY-MM-DD".length()); - assertEquals("The date string does not have proper year formatting", currentDate.indexOf("-"), "YYYY-".indexOf("-")); - assertEquals(testLocalDateTime.getYear(), Integer.parseInt(currentDate.substring(0, 4))); - assertEquals(testLocalDateTime.getMonthOfYear(), Integer.parseInt(currentDate.substring(5, 7))); - assertEquals(testLocalDateTime.getDayOfMonth(), Integer.parseInt(currentDate.substring(8, 10))); - } - - @Test - public void testDateTimeParses() { - TimeZone.setDefault(TimeZone.getTimeZone("UTC")); - - testDateTime("2016-04-13T16:26:00.000-07", 1460589960000L); - testDateTime("2015-12-16T16:09:00.000-08", 1450310940000L); // wraps day!!! - testDateTime("2015-12-16T07:09:00.000+08", 1450220940000L); // wraps day!!! - - testDateTime("2015-11-30T16:09:00.000-08", 1448928540000L); // wraps month!!! - testDateTime("2015-11-01T07:09:00.000+08", 1446332940000L); // wraps month!!! - - testDateTime("2015-12-31T16:09:00.000-08", 1451606940000L); // wraps year!!! - testDateTime("2015-01-01T07:09:00.000+08", 1420067340000L); // wraps year!!! - - testDateTime("2016-01-26T10:39:00.000-08", 1453833540000L); - - TimeZone.setDefault(TimeZone.getTimeZone("PST")); - - testDateTime("2016-04-13T16:26:00.000-07", 1460589960000L); - testDateTime("2015-12-16T16:09:00.000-08", 1450310940000L); // wraps day!!! - testDateTime("2015-12-16T07:09:00.000+08", 1450220940000L); // wraps day!!! - - testDateTime("2015-11-30T16:09:00.000-08", 1448928540000L); // wraps month!!! - testDateTime("2015-11-01T07:09:00.000+08", 1446332940000L); // wraps month!!! - - testDateTime("2015-12-31T16:09:00.000-08", 1451606940000L); // wraps year!!! - testDateTime("2015-01-01T07:09:00.000+08", 1420067340000L); // wraps year!!! - - testDateTime("2016-01-26T10:39:00.000-08", 1453833540000L); - - TimeZone.setDefault(TimeZone.getTimeZone("PDT")); - - testDateTime("2016-04-13T16:26:00.000-07", 1460589960000L); - testDateTime("2015-12-16T16:09:00.000-08", 1450310940000L); // wraps day!!! - testDateTime("2015-12-16T07:09:00.000+08", 1450220940000L); // wraps day!!! - - testDateTime("2015-11-30T16:09:00.000-08", 1448928540000L); // wraps month!!! - testDateTime("2015-11-01T07:09:00.000+08", 1446332940000L); // wraps month!!! - - testDateTime("2015-12-31T16:09:00.000-08", 1451606940000L); // wraps year!!! - testDateTime("2015-01-01T07:09:00.000+08", 1420067340000L); // wraps year!!! - - testDateTime("2016-01-26T10:39:00.000-08", 1453833540000L); - } - - private void testDateTime(String in, long test) { - Date d = DateUtils.parseDateTime(in); - - long value = d.getTime(); - - assertEquals("Fail: " + in + "(" + TimeZone.getDefault().getDisplayName() + ")", test, value); - } - - @Test - public void testTimeParses() { - //This is all kind of tricky. We need to assume J2ME level compliance, so - //dates won't every be assumed to have an intrinsic timezone, they'll be - //assumed to be in the phone's default timezone - - TimeZone.setDefault(TimeZone.getTimeZone("UTC")); - - Calendar startOfDay = Calendar.getInstance(); - startOfDay.set(Calendar.HOUR_OF_DAY, 0); - startOfDay.set(Calendar.MINUTE, 0); - startOfDay.set(Calendar.SECOND, 0); - startOfDay.set(Calendar.MILLISECOND, 0); - - long startOfDayDate = startOfDay.getTime().getTime(); - - testTime("10:00", startOfDayDate + 1000 * 60 * 60 * 10 - getOffset()); - testTime("10:00Z", startOfDayDate + 1000 * 60 * 60 * 10); - - testTime("10:00+02", startOfDayDate + 1000 * 60 * 60 * 8); - testTime("10:00-02", startOfDayDate + 1000 * 60 * 60 * 12); - - testTime("10:00+02:30", startOfDayDate + 1000 * 60 * (60 * 10 - 150)); - testTime("10:00-02:30", startOfDayDate + 1000 * 60 * (60 * 10 + 150)); - - TimeZone offsetTwoHours = TimeZone.getTimeZone("GMT+02"); - - TimeZone.setDefault(offsetTwoHours); - - testTime("10:00", startOfDayDate + 1000 * 60 * 60 * 10 - getOffset()); - testTime("10:00Z", startOfDayDate + 1000 * 60 * 60 * 10); - - testTime("10:00+02", startOfDayDate + 1000 * 60 * 60 * 8); - testTime("10:00-02", startOfDayDate + 1000 * 60 * 60 * 12); - - testTime("10:00+02:30", startOfDayDate + 1000 * 60 * (60 * 10 - 150)); - testTime("10:00-02:30", startOfDayDate + 1000 * 60 * (60 * 10 + 150)); - - TimeZone offsetMinusTwoHours = TimeZone.getTimeZone("GMT-02"); - - TimeZone.setDefault(offsetMinusTwoHours); - - testTime("14:00", startOfDayDate + 1000 * 60 * 60 * 14 - getOffset()); - testTime("14:00Z", startOfDayDate + 1000 * 60 * 60 * 14); - - testTime("14:00+02", startOfDayDate + 1000 * 60 * 60 * 12); - testTime("14:00-02", startOfDayDate + 1000 * 60 * 60 * 16); - - testTime("14:00+02:30", startOfDayDate + 1000 * 60 * (60 * 14 - 150)); - testTime("14:00-02:30", startOfDayDate + 1000 * 60 * (60 * 14 + 150)); - - - TimeZone offsetPlusHalf = TimeZone.getTimeZone("GMT+0230"); - - TimeZone.setDefault(offsetPlusHalf); - - testTime("14:00", startOfDayDate + 1000 * 60 * 60 * 14 - getOffset()); - testTime("14:00Z", startOfDayDate + 1000 * 60 * 60 * 14); - - testTime("14:00+02", startOfDayDate + 1000 * 60 * 60 * 12); - testTime("14:00-02", startOfDayDate + 1000 * 60 * 60 * 16); - - testTime("14:00+02:30", startOfDayDate + 1000 * 60 * (60 * 14 - 150)); - testTime("14:00-02:30", startOfDayDate + 1000 * 60 * (60 * 14 + 150)); - - testTime("14:00+04:00", startOfDayDate + 1000 * 60 * 60 * 10); - } - - private void testTime(String in, long test) { - Date d = DateUtils.parseTime(in); - - long value = d.getTime(); - - assertEquals("Fail: " + in + "(" + TimeZone.getDefault().getDisplayName() + ")", test, value); - } - - private long getOffset() { - DateFields df = new DateFields(); - Date d = DateUtils.getDate(df); - - return -d.getTime(); - } - - @Test - public void testParity() { - - testCycle(new Date(1300139579000L)); - testCycle(new Date(0)); - - TimeZone.setDefault(TimeZone.getTimeZone("UTC")); - - testCycle(new Date(1300139579000L)); - testCycle(new Date(0)); - - TimeZone offsetTwoHours = TimeZone.getTimeZone("GMT+02"); - - TimeZone.setDefault(offsetTwoHours); - - testCycle(new Date(1300139579000L)); - testCycle(new Date(0)); - - - TimeZone offTwoHalf = TimeZone.getTimeZone("GMT+0230"); - - TimeZone.setDefault(offTwoHalf); - - testCycle(new Date(1300139579000L)); - testCycle(new Date(0)); - - TimeZone offMinTwoHalf = TimeZone.getTimeZone("GMT-0230"); - - TimeZone.setDefault(offMinTwoHalf); - - testCycle(new Date(1300139579000L)); - testCycle(new Date(0)); - - - } - - @Test - @Ignore - // This test doesn't make sense: - // - A time has no offset nor zone. It can only have one - // when bound to a date, which is not the case - // - We're effectively binding all times to the EPOCH date - // (1970-01-01, UTC), which has no DST - public void testParseTime_with_DST() {// testFormatting - Locale.setDefault(Locale.US); - - // this is a timezone that operates DST every day of the year! - SimpleTimeZone dstTimezone = new SimpleTimeZone( - 2 * 60 * 60 * 1000, - "Europe/Athens", - Calendar.JANUARY, 1, 0, - 0, SimpleTimeZone.UTC_TIME, - Calendar.DECEMBER, 31, 0, - 24 * 60 * 60 * 1000, SimpleTimeZone.UTC_TIME, - 60 * 60 * 1000); - TimeZone.setDefault(dstTimezone); - - String time = "12:03:05.000Z"; - testTime(time, 43385000L); - - Date date = DateUtils.parseTime(time); - - DateFormat formatter = DateFormat.getTimeInstance(); - String formatted = formatter.format(date); - - // It should shift 3 hours, 2 for the zone and 1 for DST. - assertEquals("3:03:05 PM", formatted); - } - - private void testCycle(Date in) { - String formatted = DateUtils.formatDateTime(in, DateUtils.FORMAT_ISO8601); - Date out = DateUtils.parseDateTime(formatted); - assertEquals("Fail:", in.getTime(), out.getTime()); - } - - @Test - public void testFormatting() { - class LangJanSun { - private LangJanSun(String language, String january, String sunday) { - this.language = language; - this.january = january; - this.sunday = sunday; - } - - private String language; - private String january; - private String sunday; - } - - LangJanSun langJanSuns[] = new LangJanSun[]{ - new LangJanSun("en", "Jan", "Sun"), - new LangJanSun("es", "ene", "dom"), - new LangJanSun("fr", "janv.", "dim.") - }; - - for (LangJanSun ljs : langJanSuns) { - Locale.setDefault(Locale.forLanguageTag(ljs.language)); - - String month = DateUtils.format(DateFields.of(2018, 1, 1, 10, 20, 30, 400), "%b"); - assertEquals(ljs.january, month); - - // 2018-04-01 was sunday - String day = DateUtils.format(DateFields.of(2018, 4, 1, 10, 20, 30, 400), "%a"); - assertEquals(ljs.sunday, day); - } - } -} diff --git a/src/test/java/org/javarosa/test/utils/SystemHelper.java b/src/test/java/org/javarosa/test/utils/SystemHelper.java new file mode 100644 index 000000000..c10588925 --- /dev/null +++ b/src/test/java/org/javarosa/test/utils/SystemHelper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Nafundi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.javarosa.test.utils; + +import java.util.Locale; +import java.util.TimeZone; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public class SystemHelper { + public static void withTimeZone(TimeZone timeZone, Runnable block) { + withTimeZone(timeZone, __ -> block.run()); + } + + public static void withTimeZone(TimeZone timeZone, Consumer block) { + TimeZone backupZone = TimeZone.getDefault(); + TimeZone.setDefault(timeZone); + block.accept(timeZone); + TimeZone.setDefault(backupZone); + } + + public static void withLocale(Locale locale, Runnable block) { + withLocale(locale, __ -> block.run()); + } + + public static void withLocale(Locale locale, Consumer block) { + Locale backupLocale = Locale.getDefault(); + Locale.setDefault(locale); + block.accept(locale); + Locale.setDefault(backupLocale); + } + + public static void withLocaleAndTimeZone(Locale locale, TimeZone timeZone, Runnable block) { + withLocaleAndTimeZone(locale, timeZone, (__, ___) -> block.run()); + } + + public static void withLocaleAndTimeZone(Locale locale, TimeZone timeZone, BiConsumer block) { + withLocale(locale, l -> withTimeZone(timeZone, () -> block.accept(l, timeZone))); + } +} diff --git a/src/test/java/org/javarosa/xpath/expr/ToDateTest.java b/src/test/java/org/javarosa/xpath/expr/ToDateTest.java index 47d9f3a9d..bbc79827a 100644 --- a/src/test/java/org/javarosa/xpath/expr/ToDateTest.java +++ b/src/test/java/org/javarosa/xpath/expr/ToDateTest.java @@ -2,25 +2,23 @@ import static java.lang.Double.NEGATIVE_INFINITY; import static java.lang.Double.POSITIVE_INFINITY; +import static org.javarosa.test.utils.SystemHelper.withTimeZone; import static org.javarosa.xpath.expr.XPathFuncExpr.toDate; import static org.junit.Assert.assertEquals; +import java.time.ZoneId; import java.util.Date; import java.util.TimeZone; -import java.util.function.Consumer; import org.javarosa.xpath.XPathTypeMismatchException; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.LocalDateTime; -import org.junit.AfterClass; -import org.junit.BeforeClass; import org.junit.Test; +// TODO Migrate to java.time for conformity public class ToDateTest { private static final DateTime EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeZone.UTC); - - public static final TimeZone TIME_ZONE = TimeZone.getTimeZone(DateTimeZone.UTC.getID()); - private static TimeZone backupTimeZone; + public static final TimeZone PST = TimeZone.getTimeZone(ZoneId.of("America/Los_Angeles")); private static Date date(int year, int month, int day) { return new LocalDateTime(year, month, day, 0, 0, 0, 0).toDate(); @@ -30,18 +28,6 @@ private static Date date(int year, int month, int day, int hour, int minute, int return new LocalDateTime(year, month, day, hour, minute, second, milli).toDate(); } - @BeforeClass - public static void forceUTCOffset() { - // All the tests run on the UTC offset by default. - backupTimeZone = TimeZone.getDefault(); - TimeZone.setDefault(TIME_ZONE); - } - - @AfterClass - public static void restoreTimeZone() { - TimeZone.setDefault(backupTimeZone); - } - @Test public void convertsISO8601DatesWithoutPreservingTime() { assertEquals( @@ -68,17 +54,15 @@ public void convertsISO8601DatesWithOffsetPreservingTime() { @Test public void convertsTimestampsWithoutPreservingTime() { - assertEquals( + withTimeZone(TimeZone.getTimeZone("Z"), () -> assertEquals( EPOCH.withZone(DateTimeZone.getDefault()).plusDays(365).toDate(), toDate(365d, false) - ); + )); } @Test public void convertsTimestampsWithoutPreservingTimeOnLocalTimeZone() { - runOnTimeZone( - DateTimeZone.forID("America/Los_Angeles"), // a.k.a. PST (joda doesn't like the short ids) - zoneId -> { + withTimeZone(PST, tz -> { DateTime utcEpoch = EPOCH.withZone(DateTimeZone.UTC); // 1970-01-01T00:00:00 UTC DateTime properExpectedDate = utcEpoch.plusDays(365); // 1971-01-01T00:00:00 UTC // Explanation for all this: @@ -92,7 +76,7 @@ public void convertsTimestampsWithoutPreservingTimeOnLocalTimeZone() { // the same local datetime in our target zone. // 1971-01-01T00:00:00 PST (or 1971-01-01T06:00:00 UTC which is the EPOCH plus // 365 days *and 6 hours*, but will make our code work) - DateTime expectedDate = properExpectedDate.withZoneRetainFields(zoneId); + DateTime expectedDate = properExpectedDate.withZoneRetainFields(DateTimeZone.forTimeZone(tz)); assertEquals( expectedDate.toDate(), toDate(365d, false) @@ -169,10 +153,4 @@ public void anyOtherTypeThrows() { toDate(2L, false); } - private void runOnTimeZone(DateTimeZone zoneId, Consumer block) { - TimeZone backup = TimeZone.getDefault(); - TimeZone.setDefault(TimeZone.getTimeZone(zoneId.getID())); - block.accept(zoneId); - TimeZone.setDefault(backup); - } -} \ No newline at end of file +} diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index 5bb6eda73..dd0b3e89c 100644 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -24,6 +24,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; +import static org.javarosa.test.utils.SystemHelper.withLocaleAndTimeZone; import static org.javarosa.xpath.test.IFunctionHandlerHelpers.HANDLER_ADD; import static org.javarosa.xpath.test.IFunctionHandlerHelpers.HANDLER_CHECK_TYPES; import static org.javarosa.xpath.test.IFunctionHandlerHelpers.HANDLER_CONCAT; @@ -61,7 +62,6 @@ import org.javarosa.xpath.XPathUnsupportedException; import org.javarosa.xpath.expr.XPathExpression; import org.javarosa.xpath.parser.XPathSyntaxException; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -69,11 +69,10 @@ @RunWith(Parameterized.class) public class XPathEvalTest { + public static final TimeZone GMT = TimeZone.getTimeZone("GMT"); @Parameterized.Parameter(value = 0) public Locale locale; - private Locale backupLocale; - private TimeZone backupTimeZone; private EvaluationContext ec; @Parameterized.Parameters(name = "Locale {0}") @@ -86,21 +85,9 @@ public static Iterable testParametersProvider() { @Before public void setUp() { - backupLocale = Locale.getDefault(); - Locale.setDefault(locale); - backupTimeZone = TimeZone.getDefault(); - TimeZone.setDefault(TimeZone.getTimeZone("GMT")); ec = new EvaluationContext(null); } - @After - public void tearDown() { - Locale.setDefault(backupLocale); - backupLocale = null; - TimeZone.setDefault(backupTimeZone); - backupTimeZone = null; - } - @Test public void counting_functions() { testEval("count(/data/path)", buildInstance(), null, 5.0); @@ -299,27 +286,29 @@ should be tested (particularly DST changes), but it's just too hard and dates cannot reliably be compared/used across time zones (an issue with the code) any time-of-day or DST should be ignored when comparing/using a date (an issue with testing) */ - ec.addFunctionHandler(HANDLER_CONVERTIBLE); - testEval("date('2000-01-01')", DateUtils.getDate(2000, 1, 1)); - testEval("date('1945-04-26')", DateUtils.getDate(1945, 4, 26)); - testEval("date('1996-02-29')", DateUtils.getDate(1996, 2, 29)); - testEval("date('1983-09-31')", new XPathTypeMismatchException()); - testEval("date('not a date')", new XPathTypeMismatchException()); - testEval("date(0)", DateUtils.getDate(1970, 1, 1)); - testEval("date(6.5)", DateUtils.getDate(1970, 1, 7)); - testEval("date(1)", DateUtils.getDate(1970, 1, 2)); - testEval("date(-1)", DateUtils.getDate(1969, 12, 31)); - testEval("date(14127)", DateUtils.getDate(2008, 9, 5)); - testEval("date(-10252)", DateUtils.getDate(1941, 12, 7)); - testEval("date(date('1989-11-09'))", DateUtils.getDate(1989, 11, 9)); - testEval("date(true())", new XPathTypeMismatchException()); - testEval("date(convertible())", null, ec, new XPathTypeMismatchException()); - testEval("format-date('2018-01-02T10:20:30.123', \"%Y-%m-%e %H:%M:%S\")", "2018-01-2 10:20:30"); - testEval("date-time('2000-01-01T10:20:30.000')", DateUtils.getDateTimeFromString("2000-01-01T10:20:30.000")); - testEval("decimal-date-time('2000-01-01T10:20:30.000')", 10957.430902777778); - testEval("decimal-time('2000-01-01T10:20:30.000+03:00')", .30590277777810115); - testEval("decimal-date-time('-1000')", new XPathTypeMismatchException()); - testEval("decimal-date-time('-01-2019')", new XPathTypeMismatchException()); + withLocaleAndTimeZone(locale, GMT, () -> { + ec.addFunctionHandler(HANDLER_CONVERTIBLE); + testEval("date('2000-01-01')", DateUtils.getDate(2000, 1, 1)); + testEval("date('1945-04-26')", DateUtils.getDate(1945, 4, 26)); + testEval("date('1996-02-29')", DateUtils.getDate(1996, 2, 29)); + testEval("date('1983-09-31')", new XPathTypeMismatchException()); + testEval("date('not a date')", new XPathTypeMismatchException()); + testEval("date(0)", DateUtils.getDate(1970, 1, 1)); + testEval("date(6.5)", DateUtils.getDate(1970, 1, 7)); + testEval("date(1)", DateUtils.getDate(1970, 1, 2)); + testEval("date(-1)", DateUtils.getDate(1969, 12, 31)); + testEval("date(14127)", DateUtils.getDate(2008, 9, 5)); + testEval("date(-10252)", DateUtils.getDate(1941, 12, 7)); + testEval("date(date('1989-11-09'))", DateUtils.getDate(1989, 11, 9)); + testEval("date(true())", new XPathTypeMismatchException()); + testEval("date(convertible())", null, ec, new XPathTypeMismatchException()); + testEval("format-date('2018-01-02T10:20:30.123', \"%Y-%m-%e %H:%M:%S\")", "2018-01-2 10:20:30"); + testEval("date-time('2000-01-01T10:20:30.000')", DateUtils.getDateTimeFromString("2000-01-01T10:20:30.000")); + testEval("decimal-date-time('2000-01-01T10:20:30.000')", 10957.430902777778); + testEval("decimal-time('2000-01-01T10:20:30.000+03:00')", .30590277777810115); + testEval("decimal-date-time('-1000')", new XPathTypeMismatchException()); + testEval("decimal-date-time('-01-2019')", new XPathTypeMismatchException()); + }); } @Test