diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java index 3f79dc2cf7..64ab9a1c76 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java @@ -15,71 +15,91 @@ */ package org.thingsboard.script.api.tbel; +import org.mvel2.ConversionException; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; -import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.time.Instant; -import java.time.LocalDate; +import java.time.InstantSource; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.chrono.IsoChronology; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.time.temporal.ChronoUnit; -import java.time.temporal.TemporalAccessor; +import java.time.format.DateTimeParseException; +import java.util.Arrays; import java.util.Date; -import java.util.GregorianCalendar; import java.util.Locale; import java.util.function.BiFunction; -public class TbDate extends Date { +public class TbDate implements InstantSource { - private static final DateTimeFormatter isoDateFormatter = DateTimeFormatter.ofPattern( - "yyyy-MM-dd[[ ]['T']HH:mm[:ss[.SSS]][ ][XXX][Z][z][VV][O]]").withZone(ZoneId.systemDefault()); + private static Instant instant; - private static final ThreadLocal isoDateFormat = ThreadLocal.withInitial(() -> - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")); + private static String patternDefault = "%s-%s-%sT%s:%s:%s.%d%s"; + +// private static final DateTimeFormatter isoDateFormatter = DateTimeFormatter.ofPattern( +// "yyyy-MM-dd[[ ]['T']HH:mm[:ss[.SSS]][ ][XXX][Z][z][VV][O]]").withZone(ZoneId.systemDefault()); +// +// private static final ThreadLocal isoDateFormat = ThreadLocal.withInitial(() -> +// new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")); public TbDate() { - super(); + instant = Instant.now(); } public TbDate(String s) { - super(parse(s)); + try{ + if (s.length() > 0 && Character.isDigit(s.charAt(0))) { + // assuming UTC instant a la "2007-12-03T10:15:30.00Z" + instant = Instant.parse(s); + } + else { + // assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT" + instant = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(s)); + } + } catch (final DateTimeParseException ex) { + final ConversionException exception = new ConversionException("Cannot parse value [" + s + "] as instant", ex); + throw exception; + } + } + + public TbDate(long dateMilliSecond) { + instant = Instant.ofEpochMilli(dateMilliSecond); } - public TbDate(long date) { - super(date); + public TbDate(int year, int month, int date, String... tz) { + this(year, month, date, 0, 0, 0, 0, tz); } - public TbDate(int year, int month, int date) { - this(year, month, date, 0, 0, 0); + public TbDate(int year, int month, int date, int hrs, int min, String... tz) { + this(year, month, date, hrs, min, 0, 0, tz); } - public TbDate(int year, int month, int date, int hrs, int min) { - this(year, month, date, hrs, min, 0); + public TbDate(int year, int month, int date, int hrs, int min, int second, String... tz) { + this(year, month, date, hrs, min, second, 0, tz); } - public TbDate(int year, int month, int date, - int hrs, int min, int second) { - super(new GregorianCalendar(year, month, date, hrs, min, second).getTimeInMillis()); + public TbDate(int year, int month, int date, int hrs, int min, int second, int secondMilli, String... tz) { + this(createDateTimeFromPattern (year, month, date, hrs, min, second, secondMilli, tz)); + } + + @Override + public Instant instant() { + return instant; } public String toDateString() { - DateFormat formatter = DateFormat.getDateInstance(); - return formatter.format(this); + return toLocaleDateString(); } public String toTimeString() { - DateFormat formatter = DateFormat.getTimeInstance(DateFormat.LONG); - return formatter.format(this); + return toLocaleTimeString(); } public String toISOString() { - return isoDateFormat.get().format(this); + return instant.toString(); } public String toLocaleDateString() { @@ -106,9 +126,13 @@ public class TbDate extends Date { return toLocaleString(localeStr, optionsStr, (locale, options) -> DateTimeFormatter.ofLocalizedTime(options.getTimeStyle()).withLocale(locale)); } - @Override public String toLocaleString() { - return toLocaleString(null, null); + LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + return localDateTime.toString(); + } + + public LocalDateTime getLocalDateTime() { + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); } public String toLocaleString(String locale) { @@ -130,7 +154,7 @@ public class TbDate extends Date { public String toLocaleString(String localeStr, String optionsStr, BiFunction formatterBuilder) { Locale locale = StringUtils.isNotEmpty(localeStr) ? Locale.forLanguageTag(localeStr) : Locale.getDefault(); DateTimeFormatOptions options = getDateFormattingOptions(optionsStr); - ZonedDateTime zdt = this.toInstant().atZone(options.getTimeZone().toZoneId()); + ZonedDateTime zdt = this.instant().atZone(options.getTimeZone().toZoneId()); DateTimeFormatter formatter; if (StringUtils.isNotEmpty(options.getPattern())) { formatter = new DateTimeFormatterBuilder().appendPattern(options.getPattern()).toFormatter(locale); @@ -159,35 +183,53 @@ public class TbDate extends Date { return System.currentTimeMillis(); } - public static long parse(String value, String format) { - try { - DateFormat dateFormat = new SimpleDateFormat(format); - return dateFormat.parse(value).getTime(); - } catch (Exception e) { - return -1; - } - } - - public static long parse(String value) { - try { - TemporalAccessor accessor = isoDateFormatter.parseBest(value, - ZonedDateTime::from, - LocalDateTime::from, - LocalDate::from); - Instant instant = Instant.from(accessor); - return Instant.EPOCH.until(instant, ChronoUnit.MILLIS); - } catch (Exception e) { - try { - return Date.parse(value); - } catch (IllegalArgumentException e2) { - return -1; - } - } - } +// public static long parse(String value, String format) { +// try { +// DateFormat dateFormat = new SimpleDateFormat(format); +// return dateFormat.parse(value).getTime(); +// } catch (Exception e) { +// return -1; +// } +// } + public static long parseSecond() { + return instant.getEpochSecond(); + } + + public static long parseSecondMilli() { + return instant.toEpochMilli(); + } + +// public static long parse(String value) { +// try { +// TemporalAccessor accessor = isoDateFormatter.parseBest(value, +// ZonedDateTime::from, +// LocalDateTime::from, +// LocalDate::from); +// Instant instant = Instant.from(accessor); +// return Instant.EPOCH.until(instant, ChronoUnit.MILLIS); +// } catch (Exception e) { +// try { +// return Date.parse(value); +// } catch (IllegalArgumentException e2) { +// return -1; +// } +// } +// } public static long UTC(int year, int month, int date, int hrs, int min, int sec) { return Date.UTC(year - 1900, month, date, hrs, min, sec); } + private static String createDateTimeFromPattern (int year, int month, int date, int hrs, int min, int second, int secondMilli, String... tz) { + String yearStr = String.format("%04d", year); + if (yearStr.substring(0, 2).equals("00")) yearStr = "20" + yearStr.substring(2,4); + String monthStr = String.format("%02d", month); + String dateStr = String.format("%02d", date); + String hrsStr = String.format("%02d", hrs); + String minStr = String.format("%02d", min); + String secondStr = String.format("%02d", second); + String tzStr = tz.length > 0 ? Arrays.stream(tz).findFirst().get() : "Z"; + return String.format(patternDefault, yearStr, monthStr, dateStr, hrsStr, minStr, secondStr, secondMilli, tzStr); + } } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java index e31641a4fe..34643a2f86 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java @@ -24,10 +24,13 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.mvel2.ConversionException; import org.thingsboard.common.util.JacksonUtil; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.InstantSource; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.chrono.IsoChronology; @@ -46,6 +49,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; @Slf4j class TbDateTest { @@ -121,7 +126,7 @@ class TbDateTest { long ts = 1709217342987L; //Thu Feb 29 2024 14:35:42.987 GMT+0000 int offset = Calendar.getInstance().get(Calendar.ZONE_OFFSET); // for example 3600000 for GMT + 1 TbDate tbDate = new TbDate(ts - offset); - String datePrefix = "2024-02-29T14:35:42.987"; //without time zone + String datePrefix = "2024-02-29T12:35:42.987Z"; //without time zone assertThat(tbDate.toISOString()) .as("format in main thread") .startsWith(datePrefix); @@ -140,59 +145,63 @@ class TbDateTest { void testToLocaleDateString() { TbDate d = new TbDate(1693962245000L); + Assert.assertEquals("2023-09-06T01:04:05Z", d.instant().toString()); + Assert.assertEquals("06.09.23", d.toDateString()); + // Depends on time zone, so we just check it works; Assert.assertNotNull(d.toLocaleDateString()); Assert.assertNotNull(d.toLocaleDateString("en-US")); - Assert.assertEquals("9/5/23", d.toLocaleDateString("en-US", "America/New_York")); - Assert.assertEquals("23. 9. 5.", d.toLocaleDateString("ko-KR", "America/New_York")); - Assert.assertEquals("06.09.23", d.toLocaleDateString( "uk-UA", "Europe/Kiev")); - Assert.assertEquals("5\u200F/9\u200F/2023", d.toLocaleDateString( "ar-EG", "America/New_York")); + Assert.assertEquals("9/5/23", d.toLocaleDateString("en-US", "America/New_York")); + Assert.assertEquals("23. 9. 5.", d.toLocaleDateString("ko-KR", "America/New_York")); + Assert.assertEquals("06.09.23", d.toLocaleDateString( "uk-UA", "Europe/Kiev")); + Assert.assertEquals("5\u200F/9\u200F/2023", d.toLocaleDateString( "ar-EG", "America/New_York")); Assert.assertEquals("Tuesday, September 5, 2023", d.toLocaleDateString("en-US", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("dateStyle", "full") - .toString())); + .toString())); Assert.assertEquals("2023년 9월 5일 화요일", d.toLocaleDateString("ko-KR", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("dateStyle", "full") - .toString())); + .toString())); Assert.assertEquals("середа, 6 вересня 2023 р.", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode() .put("timeZone", "Europe/Kiev") .put("dateStyle", "full") - .toString())); + .toString())); Assert.assertEquals("الثلاثاء، 5 سبتمبر 2023", d.toLocaleDateString("ar-EG", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("dateStyle", "full") - .toString())); + .toString())); Assert.assertEquals("Tuesday 9/5/2023", d.toLocaleDateString("en-US", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("pattern", "EEEE M/d/yyyy") - .toString())); + .toString())); Assert.assertEquals("화요일 9/5/2023", d.toLocaleDateString("ko-KR", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("pattern", "EEEE M/d/yyyy") - .toString())); + .toString())); Assert.assertEquals("середа 9/6/2023", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode() .put("timeZone", "Europe/Kiev") .put("pattern", "EEEE M/d/yyyy") - .toString())); + .toString())); Assert.assertEquals("الثلاثاء 9/5/2023", d.toLocaleDateString("ar-EG", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("pattern", "EEEE M/d/yyyy") - .toString())); + .toString())); } @Test void testToLocaleTimeString() { TbDate d = new TbDate(1693962245000L); + Assert.assertEquals("04:04:05", d.toTimeString()); + // Depends on time zone, so we just check it works; Assert.assertNotNull(d.toLocaleTimeString()); Assert.assertNotNull(d.toLocaleTimeString("en-US")); - Assert.assertEquals("9:04:05 PM", d.toLocaleTimeString("en-US", "America/New_York")); Assert.assertEquals("오후 9:04:05", d.toLocaleTimeString("ko-KR", "America/New_York")); Assert.assertEquals("04:04:05", d.toLocaleTimeString( "uk-UA", "Europe/Kiev")); @@ -201,36 +210,36 @@ class TbDateTest { Assert.assertEquals("9:04:05 PM Eastern Daylight Time", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("timeStyle", "full") - .toString())); + .toString())); Assert.assertEquals("오후 9시 4분 5초 미 동부 하계 표준시", d.toLocaleTimeString("ko-KR", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("timeStyle", "full") - .toString())); + .toString())); Assert.assertEquals("04:04:05 за східноєвропейським літнім часом", d.toLocaleTimeString("uk-UA", JacksonUtil.newObjectNode() .put("timeZone", "Europe/Kiev") .put("timeStyle", "full") - .toString())); + .toString())); Assert.assertEquals("9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية", d.toLocaleTimeString("ar-EG", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("timeStyle", "full") - .toString())); + .toString())); Assert.assertEquals("9:04:05 PM", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("pattern", "h:mm:ss a") - .toString())); + .toString())); Assert.assertEquals("9:04:05 오후", d.toLocaleTimeString("ko-KR", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("pattern", "h:mm:ss a") - .toString())); + .toString())); Assert.assertEquals("4:04:05 дп", d.toLocaleTimeString("uk-UA", JacksonUtil.newObjectNode() .put("timeZone", "Europe/Kiev") .put("pattern", "h:mm:ss a") - .toString())); + .toString())); Assert.assertEquals("9:04:05 م", d.toLocaleTimeString("ar-EG", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("pattern", "h:mm:ss a") - .toString())); + .toString())); } @Test @@ -241,52 +250,104 @@ class TbDateTest { Assert.assertNotNull(d.toLocaleString()); Assert.assertNotNull(d.toLocaleString("en-US")); - Assert.assertEquals("9/5/23, 9:04:05 PM", d.toLocaleString("en-US", "America/New_York")); - Assert.assertEquals("23. 9. 5. 오후 9:04:05", d.toLocaleString("ko-KR", "America/New_York")); - Assert.assertEquals("06.09.23, 04:04:05", d.toLocaleString( "uk-UA", "Europe/Kiev")); + Assert.assertEquals("9/5/23, 9:04:05 PM", d.toLocaleString("en-US", "America/New_York")); + Assert.assertEquals("23. 9. 5. 오후 9:04:05", d.toLocaleString("ko-KR", "America/New_York")); + Assert.assertEquals("06.09.23, 04:04:05", d.toLocaleString( "uk-UA", "Europe/Kiev")); Assert.assertEquals("5\u200F/9\u200F/2023, 9:04:05 م", d.toLocaleString( "ar-EG", "America/New_York")); Assert.assertEquals("Tuesday, September 5, 2023 at 9:04:05 PM Eastern Daylight Time", d.toLocaleString("en-US", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("dateStyle", "full") .put("timeStyle", "full") - .toString())); + .toString())); Assert.assertEquals("2023년 9월 5일 화요일 오후 9시 4분 5초 미 동부 하계 표준시", d.toLocaleString("ko-KR", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("dateStyle", "full") .put("timeStyle", "full") - .toString())); + .toString())); Assert.assertEquals("середа, 6 вересня 2023 р. о 04:04:05 за східноєвропейським літнім часом", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode() .put("timeZone", "Europe/Kiev") .put("dateStyle", "full") .put("timeStyle", "full") - .toString())); + .toString())); Assert.assertEquals("الثلاثاء، 5 سبتمبر 2023 في 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية", d.toLocaleString("ar-EG", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("dateStyle", "full") .put("timeStyle", "full") - .toString())); + .toString())); Assert.assertEquals("9/5/2023, 9:04:05 PM", d.toLocaleString("en-US", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("pattern", "M/d/yyyy, h:mm:ss a") - .toString())); + .toString())); Assert.assertEquals("9/5/2023, 9:04:05 오후", d.toLocaleString("ko-KR", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("pattern", "M/d/yyyy, h:mm:ss a") - .toString())); + .toString())); Assert.assertEquals("9/6/2023, 4:04:05 дп", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode() .put("timeZone", "Europe/Kiev") .put("pattern", "M/d/yyyy, h:mm:ss a") - .toString())); + .toString())); Assert.assertEquals("9/5/2023, 9:04:05 م", d.toLocaleString("ar-EG", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("pattern", "M/d/yyyy, h:mm:ss a") - .toString())); + .toString())); + } + + @Test + void TestFromString () { + String stringDateUTC = "2023-09-06T01:04:05.00Z"; + TbDate d = new TbDate(stringDateUTC); + Assert.assertEquals("2023-09-06T01:04:05Z", d.instant().toString()); + String stringDateTZ = "2023-09-06T01:04:05.00+04:00"; + d = new TbDate(stringDateTZ); + Assert.assertEquals("2023-09-05T21:04:05Z", d.instant().toString()); + stringDateTZ = "2023-09-06T01:04:05.00-02:00"; + d = new TbDate(stringDateTZ); + Assert.assertEquals("2023-09-06T03:04:05Z", d.instant().toString()); + String stringDateRFC_1123 = "Sat, 3 Jun 2023 11:05:30 GMT"; + d = new TbDate(stringDateRFC_1123); + Assert.assertEquals("2023-06-03T11:05:30Z", d.instant().toString()); + + String stringDateRFC_1123_error = "Tue, 3 Jun 2023 11:05:30 GMT"; + Exception exception = assertThrows(ConversionException.class, () -> { + new TbDate(stringDateRFC_1123_error); + }); + String expectedMessage = "Cannot parse value [Tue, 3 Jun 2023 11:05:30 GMT] as instant"; + String actualMessage = exception.getMessage(); + assertTrue(actualMessage.contains(expectedMessage)); + } + + @Test + void TestParse () { + String stringDateUTC = "2023-09-06T01:04:05.345Z"; + TbDate d = new TbDate(stringDateUTC); + Assert.assertEquals(1693962245345L, d.parseSecondMilli()); + Assert.assertEquals(1693962245L, d.parseSecond()); + } + + @Test + void TestDate_Year_Moth_Date_Hs_Min_Sec () { + TbDate d = new TbDate(2023, 8, 18); + Assert.assertEquals("2023-08-18T00:00:00Z", d.instant().toString()); + d = new TbDate(2023, 9, 17, 17, 34); + Assert.assertEquals("2023-09-17T17:34:00Z", d.instant().toString()); + d = new TbDate(23, 9, 7, 8, 4); + Assert.assertEquals("2023-09-07T08:04:00Z", d.instant().toString()); + d = new TbDate(23, 9, 7, 8, 4, 5); + Assert.assertEquals("2023-09-07T08:04:05Z", d.instant().toString()); + d = new TbDate(23, 9, 7, 8, 4, 5, "+04:00"); + Assert.assertEquals("2023-09-07T04:04:05Z", d.instant().toString()); + d = new TbDate(23, 9, 7, 8, 4, 5, "-03:00"); + Assert.assertEquals("2023-09-07T11:04:05Z", d.instant().toString()); + d = new TbDate(23, 9, 7, 23, 4, 5, "-03:00"); + Assert.assertEquals("2023-09-08T02:04:05Z", d.instant().toString()); + d = new TbDate(23, 9, 7, 23, 4, 5, 567,"-03:00"); + Assert.assertEquals("2023-09-08T02:04:05.567Z", d.instant().toString()); } private static String toLocalString(TbDate d, Locale locale, ZoneId tz) { - LocalDateTime ldt = d.toInstant().atZone(tz).toLocalDateTime(); + LocalDateTime ldt = d.getLocalDateTime(); // new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter(locale)