diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml
index 3857d1d..c191f67 100644
--- a/.github/workflows/maven-publish.yml
+++ b/.github/workflows/maven-publish.yml
@@ -28,6 +28,8 @@ jobs:
- name: Publish to GitHub Packages Apache Maven
run: |
mvn deploy -B \
+ -Pgithub-repo \
+ -DskipTests \
-Pgithub-packages \
-Pgpg-sign \
-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
diff --git a/pom.xml b/pom.xml
index daa4631..2726280 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
io.github.emays
parent
- 0.0.8
+ 0.0.9-SNAPSHOT
diff --git a/src/main/java/com/mays/util/TimeUtil.java b/src/main/java/com/mays/util/TimeUtil.java
new file mode 100644
index 0000000..3088f19
--- /dev/null
+++ b/src/main/java/com/mays/util/TimeUtil.java
@@ -0,0 +1,103 @@
+package com.mays.util;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalAdjusters;
+
+public class TimeUtil {
+
+ public static boolean equalYearMonth(LocalDate d1, LocalDate d2) {
+ return d1.getYear() == d2.getYear() && d1.getMonthValue() == d2.getMonthValue();
+ }
+
+ public static String format(TemporalAccessor temporal, String pattern) {
+ return DateTimeFormatter.ofPattern(pattern).format(temporal);
+ }
+
+ // TODO Should use
+ // https://stackoverflow.com/questions/1060479/determine-whether-daylight-savings-time-dst-is-active-in-java-for-a-specified
+
+ // https://www.timeanddate.com/time/change/usa
+
+ // Daylight Saving Time (DST) in most of the United States
+
+ // starts on the 2nd Sunday in March
+
+ public static LocalDate getPossibleDstStart(int year) {
+ return LocalDate.of(year, 3, 1).with(TemporalAdjusters.firstInMonth(DayOfWeek.SUNDAY)).plusWeeks(1);
+ }
+
+ public static boolean isDstStart(LocalDate date) {
+ return date.equals(getPossibleDstStart(date.getYear()));
+ }
+
+ public static boolean isDstStart(LocalDate date, ZoneId tz) {
+ return isDstStart(date)
+ && tz.getRules().isDaylightSavings(ZonedDateTime.of(date, LocalTime.NOON, tz).toInstant());
+ }
+
+ public static LocalDate getDstStart(int year, ZoneId tz) {
+ LocalDate d = getPossibleDstStart(year);
+ if (isDstStart(d, tz))
+ return d;
+ return null;
+ }
+
+ // ends on the 1st Sunday in November
+
+ public static LocalDate getPossibleDstEnd(int year) {
+ return LocalDate.of(year, 11, 1).with(TemporalAdjusters.firstInMonth(DayOfWeek.SUNDAY));
+ }
+
+ public static boolean isDstEnd(LocalDate date) {
+ return date.equals(getPossibleDstEnd(date.getYear()));
+ }
+
+ public static boolean isDstEnd(LocalDate date, ZoneId tz) {
+ return isDstEnd(date)
+ && tz.getRules().isDaylightSavings(ZonedDateTime.of(date.minusDays(1), LocalTime.NOON, tz).toInstant());
+ }
+
+ public static LocalDate getDstEnd(int year, ZoneId tz) {
+ LocalDate d = getPossibleDstEnd(year);
+ if (isDstEnd(d, tz))
+ return d;
+ return null;
+ }
+
+ private static String getDstText(String tag, LocalDate dst, ZonedDateTime time, int lower, int upper) {
+ if (dst == null)
+ return null;
+ long days = time.toLocalDate().until(dst, ChronoUnit.DAYS);
+ if (days < lower || days > upper)
+ return null;
+ if (days < -1)
+ return tag + "ed " + -days + " days ago";
+ if (days > 1)
+ return tag + "s in " + days + " days";
+ if (days == -1)
+ return tag + "ed yesterday";
+ if (days == 0)
+ return tag + "s today";
+ if (days == 1)
+ return tag + "s tomorrow";
+ throw new IllegalStateException(time + " " + lower + " " + upper);
+ }
+
+ public static String getDstStartText(ZonedDateTime time, int lower, int upper) {
+ LocalDate dst = TimeUtil.getDstStart(time.getYear(), time.getZone());
+ return getDstText("Start", dst, time, lower, upper);
+ }
+
+ public static String getDstEndText(ZonedDateTime time, int lower, int upper) {
+ LocalDate dst = TimeUtil.getDstEnd(time.getYear(), time.getZone());
+ return getDstText("End", dst, time, lower, upper);
+ }
+
+}
diff --git a/src/test/java/com/mays/util/TimeUtilTest.java b/src/test/java/com/mays/util/TimeUtilTest.java
new file mode 100644
index 0000000..290e742
--- /dev/null
+++ b/src/test/java/com/mays/util/TimeUtilTest.java
@@ -0,0 +1,105 @@
+package com.mays.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TimeUtilTest {
+
+ @SuppressWarnings("unused")
+ private static final Logger logger = LoggerFactory.getLogger(TimeUtilTest.class);
+
+ private static final ZoneId TZ = ZoneId.of("America/New_York");
+
+ private Stream genDays() {
+ LocalDate j1 = LocalDate.of(2024, 1, 1);
+ return IntStream.range(0, j1.lengthOfYear()).mapToObj(d -> j1.plusDays(d));
+ }
+
+ private final LocalDate start = LocalDate.of(2024, 3, 10);
+ private final LocalDate end = LocalDate.of(2024, 11, 3);
+
+ @Test
+ public void isDst() {
+// TimeZoneUtil.getInstance().getZoneIds().stream().map(ZoneId::getId).sorted().forEach(logger::info);
+// ZoneId.getAvailableZoneIds().stream().filter(x -> x.contains("America")).sorted().forEach(logger::info);
+// genDays().map(LocalDate::toString).forEach(logger::info);
+ assertEquals(366, genDays().count());
+ assertEquals(1, genDays().filter(d -> TimeUtil.isDstStart(d)).count());
+ assertEquals(1, genDays().filter(d -> TimeUtil.isDstEnd(d)).count());
+ assertEquals(1, genDays().filter(d -> TimeUtil.isDstStart(d, TZ)).count());
+ assertEquals(1, genDays().filter(d -> TimeUtil.isDstEnd(d, TZ)).count());
+ assertTrue(TimeUtil.isDstStart(start));
+ assertTrue(TimeUtil.isDstStart(start, TZ));
+ assertTrue(TimeUtil.isDstEnd(end));
+ assertTrue(TimeUtil.isDstEnd(end, TZ));
+ assertEquals(start, TimeUtil.getDstStart(start.getYear(), TZ));
+ assertEquals(end, TimeUtil.getDstEnd(end.getYear(), TZ));
+ }
+
+ @Test
+ public void noDst() {
+ ZoneId tz = ZoneId.of("America/Phoenix");
+ assertEquals(0, genDays().filter(d -> TimeUtil.isDstStart(d, tz)).count());
+ assertEquals(0, genDays().filter(d -> TimeUtil.isDstEnd(d, tz)).count());
+ assertNull(TimeUtil.getDstStart(start.getYear(), tz));
+ assertNull(TimeUtil.getDstEnd(end.getYear(), tz));
+ }
+
+ @Test
+ public void dstStartText() {
+ for (int i = -50; i < 10; i++) {
+ ZonedDateTime time = ZonedDateTime.of(start.plusDays(i).atStartOfDay(), TZ);
+ String text = TimeUtil.getDstStartText(time, -7, 45);
+ if (i < -45 || i > 7) {
+ assertEquals(null, text, i + " " + text);
+ continue;
+ }
+ if (i < -1)
+ assertEquals("Starts in " + -i + " days", text, "" + i);
+ if (i == -1)
+ assertEquals("Starts tomorrow", text, "" + i);
+ if (i == 0)
+ assertEquals("Starts today", text, "" + i);
+ if (i == 1)
+ assertEquals("Started yesterday", text, "" + i);
+ if (i > 1)
+ assertEquals("Started " + i + " days ago", text, "" + i);
+
+ }
+ }
+
+ @Test
+ public void dstEndText() {
+ for (int i = -50; i < 10; i++) {
+ ZonedDateTime time = ZonedDateTime.of(end.plusDays(i).atStartOfDay(), TZ);
+ String text = TimeUtil.getDstEndText(time, -7, 45);
+ if (i < -45 || i > 7) {
+ assertEquals(null, text, i + " " + text);
+ continue;
+ }
+ if (i < -1)
+ assertEquals("Ends in " + -i + " days", text, "" + i);
+ if (i == -1)
+ assertEquals("Ends tomorrow", text, "" + i);
+ if (i == 0)
+ assertEquals("Ends today", text, "" + i);
+ if (i == 1)
+ assertEquals("Ended yesterday", text, "" + i);
+ if (i > 1)
+ assertEquals("Ended " + i + " days ago", text, "" + i);
+
+ }
+ }
+
+}