diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateFormattingOptions.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateFormattingOptions.java new file mode 100644 index 0000000000..0f4c5b625c --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateFormattingOptions.java @@ -0,0 +1,50 @@ +package org.thingsboard.script.api.tbel; + +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.StringUtils; + +import java.time.format.FormatStyle; +import java.util.TimeZone; + +@NoArgsConstructor +@Data +class DateFormattingOptions { + private static final TimeZone DEFAULT_TZ = TimeZone.getDefault(); + + private String timeZone; + private String dateStyle; + private String timeStyle; + @Getter + private String pattern; + + public DateFormattingOptions(String timeZone) { + this.timeZone = timeZone; + } + + TimeZone getTimeZone() { + return StringUtils.isNotEmpty(timeZone) ? TimeZone.getTimeZone(timeZone) : TimeZone.getDefault(); + } + + FormatStyle getDateStyle() { + return getFormatStyle(dateStyle, FormatStyle.SHORT); + } + + FormatStyle getTimeStyle() { + return getFormatStyle(timeStyle, FormatStyle.MEDIUM); + } + + private static FormatStyle getFormatStyle(String style, FormatStyle defaultStyle) { + if (StringUtils.isNotEmpty(style)) { + try { + return FormatStyle.valueOf(style.toUpperCase()); + } catch (IllegalArgumentException e) { + return defaultStyle; + } + } else { + return defaultStyle; + } + } + +} 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 c22e2c329d..4b58deab43 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 @@ -5,7 +5,7 @@ * 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 + * 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, @@ -15,6 +15,9 @@ */ package org.thingsboard.script.api.tbel; +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; @@ -22,13 +25,16 @@ import java.time.LocalDate; 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.util.Date; import java.util.GregorianCalendar; import java.util.Locale; -import java.util.TimeZone; +import java.util.function.BiFunction; +import java.util.function.Function; public class TbDate extends Date { @@ -77,15 +83,77 @@ public class TbDate extends Date { return isoDateFormat.get().format(this); } + public String toLocaleDateString() { + return toLocaleDateString(null, null); + } + + public String toLocaleDateString(String locale) { + return toLocaleDateString(locale, null); + } + + public String toLocaleDateString(String localeStr, String optionsStr) { + return toLocaleString(localeStr, optionsStr, (locale, options) -> DateTimeFormatter.ofLocalizedDate(options.getDateStyle()).withLocale(locale)); + } + + public String toLocaleTimeString() { + return toLocaleTimeString(null, null); + } + + public String toLocaleTimeString(String locale) { + return toLocaleTimeString(locale, null); + } + + public String toLocaleTimeString(String localeStr, String optionsStr) { + return toLocaleString(localeStr, optionsStr, (locale, options) -> DateTimeFormatter.ofLocalizedTime(options.getTimeStyle()).withLocale(locale)); + } + + @Override + public String toLocaleString() { + return toLocaleString(null, null); + } + public String toLocaleString(String locale) { - DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.forLanguageTag(locale)); - return formatter.format(this); + return toLocaleString(locale, null); } - public String toLocaleString(String locale, String tz) { - DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.forLanguageTag(locale)); - formatter.setTimeZone(TimeZone.getTimeZone(tz)); - return formatter.format(this); + public String toLocaleString(String localeStr, String optionsStr) { + return toLocaleString(localeStr, optionsStr, (locale, options) -> { + String formatPattern = + DateTimeFormatterBuilder.getLocalizedDateTimePattern( + options.getDateStyle(), + options.getTimeStyle(), + IsoChronology.INSTANCE, + locale); + return DateTimeFormatter.ofPattern(formatPattern, locale); + }); + } + + public String toLocaleString(String localeStr, String optionsStr, BiFunction formatterBuilder) { + Locale locale = StringUtils.isNotEmpty(localeStr) ? Locale.forLanguageTag(localeStr) : Locale.getDefault(); + DateFormattingOptions options = getDateFormattingOptions(optionsStr); + ZonedDateTime zdt = this.toInstant().atZone(options.getTimeZone().toZoneId()); + DateTimeFormatter formatter; + if (StringUtils.isNotEmpty(options.getPattern())) { + formatter = new DateTimeFormatterBuilder().appendPattern(options.getPattern()).toFormatter(locale); + } else { + formatter = formatterBuilder.apply(locale, options); + } + return formatter.format(zdt); + } + + private static DateFormattingOptions getDateFormattingOptions(String options) { + DateFormattingOptions opt = null; + if (StringUtils.isNotEmpty(options)) { + try { + opt = JacksonUtil.fromString(options, DateFormattingOptions.class); + } catch (IllegalArgumentException iae) { + opt = new DateFormattingOptions(options); + } + } + if (opt == null) { + opt = new DateFormattingOptions(); + } + return opt; } public static long now() { 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 88f1f825c1..87dd4f0ebf 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 @@ -21,12 +21,24 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.chrono.IsoChronology; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Calendar; import java.util.List; +import java.util.Locale; +import java.util.TimeZone; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -124,4 +136,174 @@ class TbDateTest { .startsWith(datePrefix); } + @Test + void testToLocaleDateString() { + TbDate d = new TbDate(1693962245000L); + + // 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("Tuesday, September 5, 2023", d.toLocaleDateString("en-US", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("dateStyle", "full") + .toString())); + Assert.assertEquals("2023년 9월 5일 화요일", d.toLocaleDateString("ko-KR", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("dateStyle", "full") + .toString())); + Assert.assertEquals("середа, 6 вересня 2023 р.", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("dateStyle", "full") + .toString())); + Assert.assertEquals("الثلاثاء، 5 سبتمبر 2023", d.toLocaleDateString("ar-EG", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("dateStyle", "full") + .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())); + Assert.assertEquals("화요일 9/5/2023", d.toLocaleDateString("ko-KR", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "EEEE M/d/yyyy") + .toString())); + Assert.assertEquals("середа 9/6/2023", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("pattern", "EEEE M/d/yyyy") + .toString())); + Assert.assertEquals("الثلاثاء 9/5/2023", d.toLocaleDateString("ar-EG", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "EEEE M/d/yyyy") + .toString())); + } + + @Test + void testToLocaleTimeString() { + TbDate d = new TbDate(1693962245000L); + + // 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")); + Assert.assertEquals("9:04:05 م", d.toLocaleTimeString( "ar-EG", "America/New_York")); + + Assert.assertEquals("9:04:05 PM Eastern Daylight Time", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("timeStyle", "full") + .toString())); + Assert.assertEquals("오후 9시 4분 5초 미 동부 하계 표준시", d.toLocaleTimeString("ko-KR", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("timeStyle", "full") + .toString())); + Assert.assertEquals("04:04:05 за східноєвропейським літнім часом", d.toLocaleTimeString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("timeStyle", "full") + .toString())); + Assert.assertEquals("9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية", d.toLocaleTimeString("ar-EG", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("timeStyle", "full") + .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())); + Assert.assertEquals("9:04:05 오후", d.toLocaleTimeString("ko-KR", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "h:mm:ss a") + .toString())); + Assert.assertEquals("4:04:05 дп", d.toLocaleTimeString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("pattern", "h:mm:ss a") + .toString())); + Assert.assertEquals("9:04:05 م", d.toLocaleTimeString("ar-EG", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "h:mm:ss a") + .toString())); + } + + @Test + void testToLocaleString() { + TbDate d = new TbDate(1693962245000L); + + // Depends on time zone, so we just check it works; + 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("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())); + 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())); + Assert.assertEquals("середа, 6 вересня 2023 р. о 04:04:05 за східноєвропейським літнім часом", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("dateStyle", "full") + .put("timeStyle", "full") + .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())); + + 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())); + 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())); + 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())); + 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())); + } + + private static String toLocalString(TbDate d, Locale locale, ZoneId tz) { + LocalDateTime ldt = d.toInstant().atZone(tz).toLocalDateTime(); + +// new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter(locale) + + String formatPattern = + DateTimeFormatterBuilder.getLocalizedDateTimePattern( + FormatStyle.SHORT, + FormatStyle.MEDIUM, + IsoChronology.INSTANCE, + locale); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatPattern, locale); + return formatter.format(ldt); + } + + private static String toLocalString2(TbDate d, Locale locale, ZoneId tz) { + DateFormat dateFormat = SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale); + dateFormat.setTimeZone(TimeZone.getTimeZone(tz)); + return dateFormat.format(d); + } + }