refactor: 重构 YearQuarter

- 修改 plusQuarters 的偏移量计算逻辑
- 同步修正测试中 plusQuarters 偏移量验证的相关逻辑
- 弃用 YearQuarter#of(Date):该方法隐式依赖系统默认时区,行为不可控
- 新增 YearQuarter#of(Date, ZoneId) 和 YearQuarter#of(Date, TimeZone) 工厂方法
This commit is contained in:
2026-05-21 23:15:26 +08:00
parent 8cc11da121
commit b0685ae32f
4 changed files with 167 additions and 12 deletions

View File

@@ -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<YearQuarter>, 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<YearQuarter>, 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) {

View File

@@ -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);
}

View File

@@ -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());
}
}
}

View File

@@ -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")));
}
// ================================