From b0685ae32faf1ae1cf4d1ddd04a2336dc0429e25 Mon Sep 17 00:00:00 2001 From: ZhouXY108 Date: Thu, 21 May 2026 23:15:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20`YearQuarter`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 plusQuarters 的偏移量计算逻辑 - 同步修正测试中 plusQuarters 偏移量验证的相关逻辑 - 弃用 YearQuarter#of(Date):该方法隐式依赖系统默认时区,行为不可控 - 新增 YearQuarter#of(Date, ZoneId) 和 YearQuarter#of(Date, TimeZone) 工厂方法 --- .../plusone/commons/time/YearQuarter.java | 47 ++++++++-- .../plusone/commons/util/DateTimeTools.java | 35 +++++++ .../commons/time/YearQuarterTests.java | 91 ++++++++++++++++++- .../commons/util/DateTimeToolsTests.java | 6 ++ 4 files changed, 167 insertions(+), 12 deletions(-) diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/time/YearQuarter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/time/YearQuarter.java index 9db1603..82ebf30 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/time/YearQuarter.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/time/YearQuarter.java @@ -23,10 +23,12 @@ import java.io.Serializable; import java.time.LocalDate; import java.time.Month; import java.time.YearMonth; +import java.time.ZoneId; import java.time.temporal.ChronoField; import java.util.Calendar; import java.util.Date; import java.util.Objects; +import java.util.TimeZone; import javax.annotation.Nullable; @@ -102,17 +104,50 @@ public final class YearQuarter implements Comparable, Serializable * * @param date 日期 * @return {@link YearQuarter} 实例 + * + * @deprecated + * 此方法使用系统默认时区,不建议使用。 + * 请使用 {@link #of(Date,ZoneId)}、{@link #of(Date,TimeZone)} 或其它工厂方法 */ + @Deprecated @StaticFactoryMethod(YearQuarter.class) public static YearQuarter of(Date date) { checkNotNull(date); - @SuppressWarnings("deprecation") final int yearValue = YEAR.checkValidIntValue(date.getYear() + 1900L); - @SuppressWarnings("deprecation") final int monthValue = date.getMonth() + 1; return new YearQuarter(yearValue, Quarter.fromMonth(monthValue)); } + /** + * 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例 + * + * @param date 日期 + * @param zoneId 时区 + * @return {@link YearQuarter} 实例 + */ + @StaticFactoryMethod(YearQuarter.class) + public static YearQuarter of(Date date, ZoneId zoneId) { + checkNotNull(date); + checkNotNull(zoneId); + LocalDate localDate = date.toInstant().atZone(zoneId).toLocalDate(); + return YearQuarter.of(localDate); + } + + /** + * 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例 + * + * @param date 日期 + * @param timeZone 时区 + * @return {@link YearQuarter} 实例 + */ + @StaticFactoryMethod(YearQuarter.class) + public static YearQuarter of(Date date, TimeZone timeZone) { + checkNotNull(date); + checkNotNull(timeZone); + LocalDate localDate = date.toInstant().atZone(timeZone.toZoneId()).toLocalDate(); + return YearQuarter.of(localDate); + } + /** * 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例 * @@ -260,11 +295,9 @@ public final class YearQuarter implements Comparable, Serializable if (quartersToAdd == 0L) { return this; } - long quarterCount = this.year * 4L + (this.quarter.getValue() - 1); - long calcQuarters = quarterCount + quartersToAdd; // safe overflow - int newYear = YEAR.checkValidIntValue(Math.floorDiv(calcQuarters, 4)); - int newQuarter = (int) Math.floorMod(calcQuarters, 4) + 1; - return new YearQuarter(newYear, Quarter.of(newQuarter)); + long quarterCount = (this.year - 1) * 4L + (this.quarter.getValue()) + quartersToAdd; + int newYear = YEAR.checkValidIntValue(Math.floorDiv(quarterCount - 1, 4) + 1); + return new YearQuarter(newYear, this.quarter.plus(quartersToAdd)); } public YearQuarter minusQuarters(long quartersToAdd) { diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/DateTimeTools.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/DateTimeTools.java index 137f8d8..89156b2 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/DateTimeTools.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/DateTimeTools.java @@ -34,6 +34,7 @@ import java.util.TimeZone; import com.google.common.collect.BoundType; import com.google.common.collect.Range; +import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod; import xyz.zhouxy.plusone.commons.time.Quarter; import xyz.zhouxy.plusone.commons.time.YearQuarter; @@ -383,17 +384,47 @@ public class DateTimeTools { * * @param date 日期 * @return 日期所在的季度 + * + * @deprecated + * 此方法使用系统默认时区,不建议使用。 + * 请使用 {@link #of(Date,ZoneId)}、{@link #of(Date,TimeZone)} 或其它工厂方法 */ + @Deprecated public static YearQuarter getQuarter(Date date) { return YearQuarter.of(date); } + /** + * 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例 + * + * @param date 日期 + * @param timeZone 时区 + * @return {@link YearQuarter} 实例 + */ + @StaticFactoryMethod(YearQuarter.class) + public static YearQuarter getQuarter(Date date, ZoneId timeZone) { + return YearQuarter.of(date, timeZone); + } + + /** + * 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例 + * + * @param date 日期 + * @param timeZone 时区 + * @return {@link YearQuarter} 实例 + */ + @StaticFactoryMethod(YearQuarter.class) + public static YearQuarter getQuarter(Date date, TimeZone timeZone) { + return YearQuarter.of(date, timeZone); + } + /** * 获取指定日期所在季度 * * @param date 日期 * @return 日期所在的季度 */ + @StaticFactoryMethod(YearQuarter.class) public static YearQuarter getQuarter(Calendar date) { return YearQuarter.of(date); } @@ -404,6 +435,7 @@ public class DateTimeTools { * @param month 月份 * @return 季度 */ + @StaticFactoryMethod(Quarter.class) public static Quarter getQuarter(Month month) { return Quarter.fromMonth(month); } @@ -415,6 +447,7 @@ public class DateTimeTools { * @param month 月 * @return 季度 */ + @StaticFactoryMethod(YearQuarter.class) public static YearQuarter getQuarter(int year, Month month) { return YearQuarter.of(YearMonth.of(year, month)); } @@ -425,6 +458,7 @@ public class DateTimeTools { * @param yearMonth 年月 * @return 季度 */ + @StaticFactoryMethod(YearQuarter.class) public static YearQuarter getQuarter(YearMonth yearMonth) { return YearQuarter.of(yearMonth); } @@ -435,6 +469,7 @@ public class DateTimeTools { * @param date 日期 * @return 日期所在的季度 */ + @StaticFactoryMethod(YearQuarter.class) public static YearQuarter getQuarter(LocalDate date) { return YearQuarter.of(date); } diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/time/YearQuarterTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/time/YearQuarterTests.java index 9e78e09..0557315 100644 --- a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/time/YearQuarterTests.java +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/time/YearQuarterTests.java @@ -21,13 +21,18 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import lombok.extern.slf4j.Slf4j; +import xyz.zhouxy.plusone.commons.util.DateTimeTools; + import java.time.DateTimeException; import java.time.LocalDate; import java.time.Month; import java.time.Year; import java.time.YearMonth; +import java.time.ZoneId; import java.util.Calendar; import java.util.Date; +import java.util.TimeZone; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -491,6 +496,82 @@ public class YearQuarterTests { }); } + // ================================ + // #region - of(Date date, ZoneId zoneId) + // ================================ + + @Test + void of_ValidDateAndZoneId_CreatesYearQuarter() { + Date date = DateTimeTools.toDate(LocalDate.of(2023, 4, 1).atStartOfDay(ZoneId.of("GMT+8"))); + YearQuarter yq08 = YearQuarter.of(date, ZoneId.of("GMT+8")); + assertEquals(2023, yq08.getYear()); + assertEquals(2, yq08.getQuarterValue()); + assertSame(Quarter.Q2, yq08.getQuarter()); + + YearQuarter yq00 = YearQuarter.of(date, ZoneId.of("GMT+0")); + assertEquals(2023, yq00.getYear()); + assertEquals(1, yq00.getQuarterValue()); + assertSame(Quarter.Q1, yq00.getQuarter()); + } + + @Test + void of_NullDateAndZoneId_NullPointerException() { + Date date = null; + assertThrows(NullPointerException.class, () -> { + YearQuarter.of(date, ZoneId.systemDefault()); + }); + } + + @Test + void of_ValidDateAndNullZoneId_NullPointerException() { + Date date = new Date(); + assertThrows(NullPointerException.class, () -> { + YearQuarter.of(date, (ZoneId) null); + }); + } + + // ================================ + // #endregion - of(Date date, ZoneId zoneId) + // ================================ + + // ================================ + // #region - of(Date date, TimeZone timeZone) + // ================================ + + @Test + void of_ValidDateAndTimeZone_CreatesYearQuarter() { + Date date = DateTimeTools.toDate(LocalDate.of(2023, 4, 1).atStartOfDay(ZoneId.of("GMT+8"))); + YearQuarter yq08 = YearQuarter.of(date, TimeZone.getTimeZone("GMT+8")); + assertEquals(2023, yq08.getYear()); + assertEquals(2, yq08.getQuarterValue()); + assertSame(Quarter.Q2, yq08.getQuarter()); + + YearQuarter yq00 = YearQuarter.of(date, TimeZone.getTimeZone("GMT+0")); + assertEquals(2023, yq00.getYear()); + assertEquals(1, yq00.getQuarterValue()); + assertSame(Quarter.Q1, yq00.getQuarter()); + } + + @Test + void of_NullDateAndTimeZone_NullPointerException() { + Date date = null; + assertThrows(NullPointerException.class, () -> { + YearQuarter.of(date, TimeZone.getDefault()); + }); + } + + @Test + void of_ValidDateAndNullTimeZone_NullPointerException() { + Date date = new Date(); + assertThrows(NullPointerException.class, () -> { + YearQuarter.of(date, (TimeZone) null); + }); + } + + // ================================ + // #endregion - of(Date date, ZoneId zoneId) + // ================================ + // ================================ // #endregion - of(Date date) // ================================ @@ -927,14 +1008,14 @@ public class YearQuarterTests { YearQuarter minus = yq1.minusQuarters(-quartersToAdd); assertEquals(plus, minus); - // offset: 表示自 公元 0000年以来,经历了多少季度。所以 0 表示 -0001,Q4; 1 表示 0000 Q1 - long offset = (year * 4L + quarter) + quartersToAdd; + // offset: 表示自 公元 1 年以来,经历了多少季度。所以 0 表示 0000,Q4; 1 表示 0001,Q1 + long offset = ((year - 1) * 4L + quarter) + quartersToAdd; if (offset > 0) { - assertEquals((offset - 1) / 4, plus.getYear()); + assertEquals((offset - 1) / 4 + 1, plus.getYear()); assertEquals(((offset - 1) % 4) + 1, plus.getQuarterValue()); } else { - assertEquals((offset / 4 - 1), plus.getYear()); - assertEquals((4 + offset % 4), plus.getQuarterValue()); + assertEquals(offset / 4, plus.getYear()); + assertEquals(4 + offset % 4, plus.getQuarterValue()); } } } diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/util/DateTimeToolsTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/util/DateTimeToolsTests.java index f9da4ec..92152fb 100644 --- a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/util/DateTimeToolsTests.java +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/util/DateTimeToolsTests.java @@ -271,6 +271,12 @@ class DateTimeToolsTests { assertEquals(expectedYearQuarter, DateTimeTools.getQuarter(2024, Month.DECEMBER)); assertEquals(expectedYearQuarter, DateTimeTools.getQuarter(YearMonth.of(2024, Month.DECEMBER))); assertEquals(expectedYearQuarter, DateTimeTools.getQuarter(LOCAL_DATE)); + + Date date = DateTimeTools.toDate(LocalDate.of(2026, 1, 1).atStartOfDay(ZoneId.of("GMT+8"))); + assertEquals(YearQuarter.of(2026, 1), DateTimeTools.getQuarter(date, TimeZone.getTimeZone("GMT+8"))); + assertEquals(YearQuarter.of(2026, 1), DateTimeTools.getQuarter(date, ZoneId.of("GMT+8"))); + assertEquals(YearQuarter.of(2025, 4), DateTimeTools.getQuarter(date, TimeZone.getTimeZone("GMT+0"))); + assertEquals(YearQuarter.of(2025, 4), DateTimeTools.getQuarter(date, ZoneId.of("GMT+0"))); } // ================================