From c60ee19ac05843d2bc92ccb86d00074cfbb5761c Mon Sep 17 00:00:00 2001
From: zhouxy108 <zhou_luquan@163.com>
Date: Sun, 23 Mar 2025 15:57:13 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=AD=A3=E5=BA=A6?=
 =?UTF-8?q?=E7=9B=B8=E5=85=B3=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

1. 扩展枚举 Quarter
2. 参考 JDK 的 YearMonth,新增 YearQuarter
3. 添加 junit-jupiter-params 测试依赖,以便参数化测试
---
 .../java/cn/hutool/core/date/Quarter.java     |  156 +++
 .../java/cn/hutool/core/date/YearQuarter.java |  365 ++++++
 .../java/cn/hutool/core/date/QuarterTest.java |  268 ++++
 .../cn/hutool/core/date/YearQuarterTest.java  | 1148 +++++++++++++++++
 pom.xml                                       |    6 +
 5 files changed, 1943 insertions(+)
 create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/YearQuarter.java
 create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/QuarterTest.java
 create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/YearQuarterTest.java

diff --git a/hutool-core/src/main/java/cn/hutool/core/date/Quarter.java b/hutool-core/src/main/java/cn/hutool/core/date/Quarter.java
index 3a0985109..4757794aa 100644
--- a/hutool-core/src/main/java/cn/hutool/core/date/Quarter.java
+++ b/hutool-core/src/main/java/cn/hutool/core/date/Quarter.java
@@ -1,5 +1,11 @@
 package cn.hutool.core.date;
 
+import java.time.DateTimeException;
+import java.time.MonthDay;
+import java.time.temporal.ChronoField;
+
+import cn.hutool.core.lang.Assert;
+
 /**
  * 季度枚举
  *
@@ -25,8 +31,14 @@ public enum Quarter {
 	// ---------------------------------------------------------------
 	private final int value;
 
+	private final int firstMonth;
+	private final int lastMonth;
+
 	Quarter(int value) {
 		this.value = value;
+
+		this.lastMonth = value * 3;
+		this.firstMonth = lastMonth - 2;
 	}
 
 	public int getValue() {
@@ -58,4 +70,148 @@ public enum Quarter {
 				return null;
 		}
 	}
+
+	/**
+	 * 根据给定的月份值返回对应的季度
+	 *
+	 * @param monthValue 月份值,取值范围为1到12
+	 * @return 对应的季度
+	 * @throws IllegalArgumentException 如果月份值不在有效范围内(1到12),将抛出异常
+	 */
+	public static Quarter fromMonth(int monthValue) {
+		ChronoField.MONTH_OF_YEAR.checkValidValue(monthValue);
+		return of(computeQuarterValueInternal(monthValue));
+	}
+
+	/**
+	 * 根据给定的月份返回对应的季度
+	 *
+	 * @param month 月份
+	 * @return 对应的季度
+	 */
+	public static Quarter fromMonth(Month month) {
+		Assert.notNull(month);
+		final int monthValue = month.getValue();
+		return of(computeQuarterValueInternal(monthValue));
+	}
+
+	/**
+	 * 根据指定的年份,获取一个新的 YearQuarter 实例
+	 * 此方法允许在保持当前季度信息不变的情况下,更改年份
+	 *
+	 * @param year 指定的年份
+	 * @return 返回一个新的 YearQuarter 实例,年份更新为指定的年份
+	 */
+	public final YearQuarter atYear(int year) {
+		return YearQuarter.of(year, this);
+	}
+
+	// StaticFactoryMethods end
+
+	// computes
+
+	/**
+	 * 加上指定数量的季度
+	 *
+	 * @param quarters 所添加的季度数量
+	 * @return 计算结果
+	 */
+	public Quarter plus(long quarters) {
+		final int amount = (int) ((quarters % 4) + 4);
+		return Quarter.values()[(ordinal() + amount) % 4];
+	}
+
+	/**
+	 * 减去指定数量的季度
+	 * @param quarters 所减去的季度数量
+	 * @return 计算结果
+	 */
+	public Quarter minus(long quarters) {
+		return plus(-(quarters % 4));
+	}
+
+	// computes end
+
+	// Getters
+
+	/**
+	 * 该季度的第一个月
+	 *
+	 * @return 结果
+	 */
+	public Month firstMonth() {
+		return Month.of(firstMonthValue() - 1);
+	}
+
+	/**
+	 * 该季度的第一个月
+	 *
+	 * @return 结果。月份值从 1 开始,1 表示 1月,以此类推。
+	 */
+	public int firstMonthValue() {
+		return this.firstMonth;
+	}
+
+	/**
+	 * 该季度最后一个月
+	 *
+	 * @return 结果
+	 */
+	public Month lastMonth() {
+		return Month.of(lastMonthValue() - 1);
+	}
+
+	/**
+	 * 该季度最后一个月
+	 *
+	 * @return 结果。1 表示 1月,以此类推。
+	 */
+	public int lastMonthValue() {
+		return this.lastMonth;
+	}
+
+	/**
+	 * 该季度的第一天
+	 *
+	 * @return 结果
+	 */
+	public MonthDay firstMonthDay() {
+		return MonthDay.of(firstMonthValue(), 1);
+	}
+
+	/**
+	 * 该季度的最后一天
+	 *
+	 * @return 结果
+	 */
+	public MonthDay lastMonthDay() {
+		// 季度的最后一个月不可能是 2 月,不考虑闰年
+		final Month month = lastMonth();
+		return MonthDay.of(month.toJdkMonth(), month.getLastDay(false));
+	}
+
+	// Getters end
+
+	/**
+	 * 检查季度的值。(1~4)
+	 * @param value 季度值
+	 * @return 在取值范围内的值
+	 */
+	public static int checkValidIntValue(int value) {
+		Assert.isTrue(value >= 1 && value <= 4,
+				() -> new DateTimeException("Invalid value for Quarter: " + value));
+		return value;
+	}
+
+	// Internal
+
+	/**
+	 * 计算给定月份对应的季度值
+	 *
+	 * @param monthValue 月份值,取值范围为1到12
+	 * @return 对应的季度值
+	 */
+	private static int computeQuarterValueInternal(int monthValue) {
+		return (monthValue - 1) / 3 + 1;
+	}
 }
diff --git a/hutool-core/src/main/java/cn/hutool/core/date/YearQuarter.java b/hutool-core/src/main/java/cn/hutool/core/date/YearQuarter.java
new file mode 100644
index 000000000..969eead0d
--- /dev/null
+++ b/hutool-core/src/main/java/cn/hutool/core/date/YearQuarter.java
@@ -0,0 +1,365 @@
+package cn.hutool.core.date;
+
+import static java.time.temporal.ChronoField.YEAR;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.temporal.ChronoField;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * 表示年份与季度
+ *
+ * @author ZhouXY
+ */
+public final class YearQuarter implements Comparable<YearQuarter>, Serializable {
+	private static final long serialVersionUID = 3804145964419489753L;
+
+	/** 年份 */
+	private final int year;
+	/** 季度 */
+	private final Quarter quarter;
+	/** 季度开始日期 */
+	private final LocalDate firstDate;
+	/** 季度结束日期 */
+	private final LocalDate lastDate;
+
+	private YearQuarter(int year, Quarter quarter) {
+		this.year = year;
+		this.quarter = quarter;
+		this.firstDate = quarter.firstMonthDay().atYear(year);
+		this.lastDate = quarter.lastMonthDay().atYear(year);
+	}
+
+	// #region - StaticFactory
+
+	/**
+	 * 根据指定年份与季度,创建 {@link YearQuarter} 实例
+	 *
+	 * @param year 年份
+	 * @param quarter 季度
+	 * @return {@link YearQuarter} 实例
+	 */
+	public static YearQuarter of(int year, int quarter) {
+		int yearValue = YEAR.checkValidIntValue(year);
+		int quarterValue = Quarter.checkValidIntValue(quarter);
+		return new YearQuarter(yearValue, Quarter.of(quarterValue));
+	}
+
+	/**
+	 * 根据指定年份与季度,创建 {@link YearQuarter} 实例
+	 *
+	 * @param year 年份
+	 * @param quarter 季度
+	 * @return {@link YearQuarter} 实例
+	 */
+	public static YearQuarter of(int year, Quarter quarter) {
+		return new YearQuarter(YEAR.checkValidIntValue(year), Objects.requireNonNull(quarter));
+	}
+
+	/**
+	 * 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例
+	 *
+	 * @param date 日期
+	 * @return {@link YearQuarter} 实例
+	 */
+	public static YearQuarter of(LocalDate date) {
+		Objects.requireNonNull(date);
+		return new YearQuarter(date.getYear(), Quarter.fromMonth(date.getMonthValue()));
+	}
+
+	/**
+	 * 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例
+	 *
+	 * @param date 日期
+	 * @return {@link YearQuarter} 实例
+	 */
+	public static YearQuarter of(Date date) {
+		Objects.requireNonNull(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 日期
+	 * @return {@link YearQuarter} 实例
+	 */
+	public static YearQuarter of(Calendar date) {
+		Objects.requireNonNull(date);
+		final int yearValue = ChronoField.YEAR.checkValidIntValue(date.get(Calendar.YEAR));
+		final int monthValue = date.get(Calendar.MONTH) + 1;
+		return new YearQuarter(yearValue, Quarter.fromMonth(monthValue));
+	}
+
+	/**
+	 * 根据指定年月,判断其所在的年份与季度,创建 {@link YearQuarter} 实例
+	 *
+	 * @param yearMonth 年月
+	 * @return {@link YearQuarter} 实例
+	 */
+	public static YearQuarter of(YearMonth yearMonth) {
+		Objects.requireNonNull(yearMonth);
+		return of(yearMonth.getYear(), Quarter.fromMonth(yearMonth.getMonthValue()));
+	}
+
+	/**
+	 * 当前年季
+	 *
+	 * @return 当前年季
+	 */
+
+	public static YearQuarter now() {
+		return of(LocalDate.now());
+	}
+
+	// #endregion
+
+	// #region - Getters
+
+	/**
+	 * 年份
+	 * @return 年份
+	 */
+	public int getYear() {
+		return this.year;
+	}
+
+	/**
+	 * 季度
+	 * @return 季度
+	 */
+	public Quarter getQuarter() {
+		return this.quarter;
+	}
+
+	/**
+	 * 季度值。从 1 开始。
+	 * @return 季度值
+	 */
+	public int getQuarterValue() {
+		return this.quarter.getValue();
+	}
+
+	/**
+	 * 该季度第一个月
+	 * @return {@link YearMonth} 对象
+	 */
+	public YearMonth firstYearMonth() {
+		return YearMonth.of(this.year, this.quarter.firstMonthValue());
+	}
+
+	/**
+	 * 该季度第一个月
+	 * @return {@link Month} 对象
+	 */
+	public Month firstMonth() {
+		return this.quarter.firstMonth();
+	}
+
+	/**
+	 * 该季度的第一个月
+	 * @return 结果。月份值从 1 开始,1 表示 1月,以此类推。
+	 */
+	public int firstMonthValue() {
+		return this.quarter.firstMonthValue();
+	}
+
+	/**
+	 * 该季度的最后一个月
+	 * @return {@link YearMonth} 对象
+	 */
+	public YearMonth lastYearMonth() {
+		return YearMonth.of(this.year, this.quarter.lastMonthValue());
+	}
+
+	/**
+	 * 该季度的最后一个月
+	 * @return {@link Month} 对象
+	 */
+	public Month lastMonth() {
+		return this.quarter.lastMonth();
+	}
+
+	/**
+	 * 该季度的最后一个月
+	 * @return 结果。月份值从 1 开始,1 表示 1月,以此类推。
+	 */
+	public int lastMonthValue() {
+		return this.quarter.lastMonthValue();
+	}
+
+	/**
+	 * 该季度的第一天
+	 * @return {@link LocalDate} 对象
+	 */
+	public LocalDate firstDate() {
+		return firstDate;
+	}
+
+	/**
+	 * 该季度的最后一天
+	 * @return {@link LocalDate} 对象
+	 */
+	public LocalDate lastDate() {
+		return lastDate;
+	}
+
+	// #endregion
+
+	// #region - computes
+
+	/**
+	 * 添加季度
+	 * @param quartersToAdd 要添加的季度数
+	 * @return 计算结果
+	 */
+	public YearQuarter plusQuarters(long quartersToAdd) {
+		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));
+	}
+
+	/**
+	 * 减去季度
+	 * @param quartersToMinus 要减去的季度数
+	 * @return 计算结果
+	 */
+	public YearQuarter minusQuarters(long quartersToMinus) {
+		return plusQuarters(-quartersToMinus);
+	}
+
+	/**
+	 * 下一个季度
+	 * @return 结果
+	 */
+	public YearQuarter nextQuarter() {
+		return plusQuarters(1L);
+	}
+
+	/**
+	 * 上一个季度
+	 * @return 结果
+	 */
+	public YearQuarter lastQuarter() {
+		return minusQuarters(1L);
+	}
+
+	/**
+	 * 添加年份
+	 * @param yearsToAdd 要添加的年份数
+	 * @return 计算结果
+	 */
+	public YearQuarter plusYears(long yearsToAdd) {
+		if (yearsToAdd == 0L) {
+			return this;
+		}
+		int newYear = YEAR.checkValidIntValue(this.year + yearsToAdd); // safe overflow
+		return new YearQuarter(newYear, this.quarter);
+	}
+
+	/**
+	 * 减去年份
+	 * @param yearsToMinus 要减去的年份数
+	 * @return 计算结果
+	 */
+	public YearQuarter minusYears(long yearsToMinus) {
+		return plusYears(-yearsToMinus);
+	}
+
+	/**
+	 * 下一年同季度
+	 * @return 计算结果
+	 */
+	public YearQuarter nextYear() {
+		return plusYears(1L);
+	}
+
+	/**
+	 * 上一年同季度
+	 * @return 计算结果
+	 */
+	public YearQuarter lastYear() {
+		return minusYears(1L);
+	}
+
+	// #endregion
+
+	// #region - hashCode & equals
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(year, quarter);
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		YearQuarter other = (YearQuarter) obj;
+		return year == other.year && quarter == other.quarter;
+	}
+
+	// #endregion
+
+	// #region - compare
+
+	@Override
+	public int compareTo(YearQuarter other) {
+		int cmp = (this.year - other.year);
+		if (cmp == 0) {
+			cmp = this.quarter.compareTo(other.quarter);
+		}
+		return cmp;
+	}
+
+	/**
+	 * 判断是否在指定年份季度之前
+	 * @param other 比较对象
+	 * @return 结果
+	 */
+	public boolean isBefore(YearQuarter other) {
+		return this.compareTo(other) < 0;
+	}
+
+	/**
+	 * 判断是否在指定年份季度之后
+	 * @param other 比较对象
+	 * @return 结果
+	 */
+	public boolean isAfter(YearQuarter other) {
+		return this.compareTo(other) > 0;
+	}
+
+	// #endregion
+
+	// #region - toString
+
+	/**
+	 * 返回 {@link YearQuarter} 的字符串表示形式,如 "2024 Q3"
+	 *
+	 * @return {@link YearQuarter} 的字符串表示形式
+	 */
+	@Override
+	public String toString() {
+		return this.year + " " + this.quarter.name();
+	}
+
+	// #endregion
+}
diff --git a/hutool-core/src/test/java/cn/hutool/core/date/QuarterTest.java b/hutool-core/src/test/java/cn/hutool/core/date/QuarterTest.java
new file mode 100644
index 000000000..2cdcf797b
--- /dev/null
+++ b/hutool-core/src/test/java/cn/hutool/core/date/QuarterTest.java
@@ -0,0 +1,268 @@
+package cn.hutool.core.date;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.time.MonthDay;
+
+import org.junit.jupiter.api.Test;
+
+public class QuarterTest {
+
+	@Test
+	void testQ1() {
+		Quarter quarter = Quarter.of(1);
+		assertSame(Quarter.Q1, quarter);
+		assertSame(quarter, Quarter.valueOf("Q1"));
+		assertEquals(1, quarter.getValue());
+		assertEquals("Q1", quarter.name());
+
+		assertNull(Quarter.of(0));
+
+		// ==========
+
+		int firstMonthValue = quarter.firstMonthValue();
+		assertEquals(1, firstMonthValue);
+
+		Month firstMonth = quarter.firstMonth();
+		assertEquals(Month.JANUARY, firstMonth);
+
+		// ==========
+
+		int lastMonthValue = quarter.lastMonthValue();
+		assertEquals(3, lastMonthValue);
+
+		Month lastMonth = quarter.lastMonth();
+		assertEquals(Month.MARCH, lastMonth);
+
+		// ==========
+
+		MonthDay firstMonthDay = quarter.firstMonthDay();
+		assertEquals(firstMonthDay, MonthDay.of(1, 1));
+
+		MonthDay lastMonthDay = quarter.lastMonthDay();
+		assertEquals(lastMonthDay, MonthDay.of(3, 31));
+	}
+
+	@Test
+	void testQ2() {
+		Quarter quarter = Quarter.of(2);
+		assertSame(Quarter.Q2, quarter);
+		assertSame(quarter, Quarter.valueOf("Q2"));
+		assertEquals(2, quarter.getValue());
+		assertEquals("Q2", quarter.name());
+
+		assertNull(Quarter.of(5));
+
+		// ==========
+
+		int firstMonthValue = quarter.firstMonthValue();
+		assertEquals(4, firstMonthValue);
+
+		Month firstMonth = quarter.firstMonth();
+		assertEquals(Month.APRIL, firstMonth);
+
+		// ==========
+
+		int lastMonthValue = quarter.lastMonthValue();
+		assertEquals(6, lastMonthValue);
+
+		Month lastMonth = quarter.lastMonth();
+		assertEquals(Month.JUNE, lastMonth);
+
+		// ==========
+
+		MonthDay firstMonthDay = quarter.firstMonthDay();
+		assertEquals(firstMonthDay, MonthDay.of(4, 1));
+
+		MonthDay lastMonthDay = quarter.lastMonthDay();
+		assertEquals(lastMonthDay, MonthDay.of(6, 30));
+	}
+
+	@Test
+	void testQ3() {
+		Quarter quarter = Quarter.of(3);
+		assertSame(Quarter.Q3, quarter);
+		assertSame(quarter, Quarter.valueOf("Q3"));
+		assertEquals(3, quarter.getValue());
+		assertEquals("Q3", quarter.name());
+
+		assertThrows(IllegalArgumentException.class, () -> {
+			Quarter.valueOf("Abc");
+		});
+
+		// ==========
+
+		int firstMonthValue = quarter.firstMonthValue();
+		assertEquals(7, firstMonthValue);
+
+		Month firstMonth = quarter.firstMonth();
+		assertEquals(Month.JULY, firstMonth);
+
+		// ==========
+
+		int lastMonthValue = quarter.lastMonthValue();
+		assertEquals(9, lastMonthValue);
+
+		Month lastMonth = quarter.lastMonth();
+		assertEquals(Month.SEPTEMBER, lastMonth);
+
+		// ==========
+
+		MonthDay firstMonthDay = quarter.firstMonthDay();
+		assertEquals(firstMonthDay, MonthDay.of(7, 1));
+
+		MonthDay lastMonthDay = quarter.lastMonthDay();
+		assertEquals(lastMonthDay, MonthDay.of(9, 30));
+	}
+
+	@Test
+	void testQ4() {
+		Quarter quarter = Quarter.of(4);
+		assertSame(Quarter.Q4, quarter);
+		assertSame(quarter, Quarter.valueOf("Q4"));
+		assertEquals(4, quarter.getValue());
+		assertEquals("Q4", quarter.name());
+
+		assertThrows(IllegalArgumentException.class, () -> {
+			Quarter.valueOf("Q5");
+		});
+
+		// ==========
+
+		int firstMonthValue = quarter.firstMonthValue();
+		assertEquals(10, firstMonthValue);
+
+		Month firstMonth = quarter.firstMonth();
+		assertEquals(Month.OCTOBER, firstMonth);
+
+		// ==========
+
+		int lastMonthValue = quarter.lastMonthValue();
+		assertEquals(12, lastMonthValue);
+		Month lastMonth = quarter.lastMonth();
+		assertEquals(Month.DECEMBER, lastMonth);
+
+		// ==========
+
+		MonthDay firstMonthDay = quarter.firstMonthDay();
+		assertEquals(firstMonthDay, MonthDay.of(10, 1));
+
+		MonthDay lastMonthDay = quarter.lastMonthDay();
+		assertEquals(lastMonthDay, MonthDay.of(12, 31));
+	}
+
+	@Test
+	void testPlusZeroAndPositiveRealNumbers() {
+		for (int i = 0; i < 100; i += 4) {
+			assertEquals(Quarter.Q1, Quarter.Q1.plus(i));
+			assertEquals(Quarter.Q2, Quarter.Q2.plus(i));
+			assertEquals(Quarter.Q3, Quarter.Q3.plus(i));
+			assertEquals(Quarter.Q4, Quarter.Q4.plus(i));
+		}
+		for (int i = 1; i < 100 + 1; i += 4) {
+			assertEquals(Quarter.Q2, Quarter.Q1.plus(i));
+			assertEquals(Quarter.Q3, Quarter.Q2.plus(i));
+			assertEquals(Quarter.Q4, Quarter.Q3.plus(i));
+			assertEquals(Quarter.Q1, Quarter.Q4.plus(i));
+		}
+		for (int i = 2; i < 100 + 2; i += 4) {
+			assertEquals(Quarter.Q3, Quarter.Q1.plus(i));
+			assertEquals(Quarter.Q4, Quarter.Q2.plus(i));
+			assertEquals(Quarter.Q1, Quarter.Q3.plus(i));
+			assertEquals(Quarter.Q2, Quarter.Q4.plus(i));
+		}
+		for (int i = 3; i < 100 + 3; i += 4) {
+			assertEquals(Quarter.Q4, Quarter.Q1.plus(i));
+			assertEquals(Quarter.Q1, Quarter.Q2.plus(i));
+			assertEquals(Quarter.Q2, Quarter.Q3.plus(i));
+			assertEquals(Quarter.Q3, Quarter.Q4.plus(i));
+		}
+	}
+
+	@Test
+	void testPlusZeroAndNegativeNumber() {
+		for (int i = 0; i > -100; i -= 4) {
+			assertEquals(Quarter.Q1, Quarter.Q1.plus(i));
+			assertEquals(Quarter.Q2, Quarter.Q2.plus(i));
+			assertEquals(Quarter.Q3, Quarter.Q3.plus(i));
+			assertEquals(Quarter.Q4, Quarter.Q4.plus(i));
+		}
+		for (int i = -1; i > -(100 + 1); i -= 4) {
+			assertEquals(Quarter.Q4, Quarter.Q1.plus(i));
+			assertEquals(Quarter.Q1, Quarter.Q2.plus(i));
+			assertEquals(Quarter.Q2, Quarter.Q3.plus(i));
+			assertEquals(Quarter.Q3, Quarter.Q4.plus(i));
+		}
+		for (int i = -2; i > -(100 + 2); i -= 4) {
+			assertEquals(Quarter.Q3, Quarter.Q1.plus(i));
+			assertEquals(Quarter.Q4, Quarter.Q2.plus(i));
+			assertEquals(Quarter.Q1, Quarter.Q3.plus(i));
+			assertEquals(Quarter.Q2, Quarter.Q4.plus(i));
+		}
+		for (int i = -3; i > -(100 + 3); i -= 4) {
+			assertEquals(Quarter.Q2, Quarter.Q1.plus(i));
+			assertEquals(Quarter.Q3, Quarter.Q2.plus(i));
+			assertEquals(Quarter.Q4, Quarter.Q3.plus(i));
+			assertEquals(Quarter.Q1, Quarter.Q4.plus(i));
+		}
+	}
+
+	@Test
+	void testMinusZeroAndNegativeNumber() {
+		for (int i = 0; i < 100; i += 4) {
+			assertEquals(Quarter.Q1, Quarter.Q1.minus(i));
+			assertEquals(Quarter.Q2, Quarter.Q2.minus(i));
+			assertEquals(Quarter.Q3, Quarter.Q3.minus(i));
+			assertEquals(Quarter.Q4, Quarter.Q4.minus(i));
+		}
+		for (int i = 1; i < 100 + 1; i += 4) {
+			assertEquals(Quarter.Q4, Quarter.Q1.minus(i));
+			assertEquals(Quarter.Q1, Quarter.Q2.minus(i));
+			assertEquals(Quarter.Q2, Quarter.Q3.minus(i));
+			assertEquals(Quarter.Q3, Quarter.Q4.minus(i));
+		}
+		for (int i = 2; i < 100 + 2; i += 4) {
+			assertEquals(Quarter.Q3, Quarter.Q1.minus(i));
+			assertEquals(Quarter.Q4, Quarter.Q2.minus(i));
+			assertEquals(Quarter.Q1, Quarter.Q3.minus(i));
+			assertEquals(Quarter.Q2, Quarter.Q4.minus(i));
+		}
+		for (int i = 3; i < 100 + 3; i += 4) {
+			assertEquals(Quarter.Q2, Quarter.Q1.minus(i));
+			assertEquals(Quarter.Q3, Quarter.Q2.minus(i));
+			assertEquals(Quarter.Q4, Quarter.Q3.minus(i));
+			assertEquals(Quarter.Q1, Quarter.Q4.minus(i));
+		}
+	}
+
+	@Test
+	void testMinusZeroAndPositiveRealNumbers() {
+		for (int i = 0; i > -100; i -= 4) {
+			assertEquals(Quarter.Q1, Quarter.Q1.minus(i));
+			assertEquals(Quarter.Q2, Quarter.Q2.minus(i));
+			assertEquals(Quarter.Q3, Quarter.Q3.minus(i));
+			assertEquals(Quarter.Q4, Quarter.Q4.minus(i));
+		}
+		for (int i = -1; i > -(100 + 1); i -= 4) {
+			assertEquals(Quarter.Q2, Quarter.Q1.minus(i));
+			assertEquals(Quarter.Q3, Quarter.Q2.minus(i));
+			assertEquals(Quarter.Q4, Quarter.Q3.minus(i));
+			assertEquals(Quarter.Q1, Quarter.Q4.minus(i));
+		}
+		for (int i = -2; i > -(100 + 2); i -= 4) {
+			assertEquals(Quarter.Q3, Quarter.Q1.minus(i));
+			assertEquals(Quarter.Q4, Quarter.Q2.minus(i));
+			assertEquals(Quarter.Q1, Quarter.Q3.minus(i));
+			assertEquals(Quarter.Q2, Quarter.Q4.minus(i));
+		}
+		for (int i = -3; i > -(100 + 3); i -= 4) {
+			assertEquals(Quarter.Q4, Quarter.Q1.minus(i));
+			assertEquals(Quarter.Q1, Quarter.Q2.minus(i));
+			assertEquals(Quarter.Q2, Quarter.Q3.minus(i));
+			assertEquals(Quarter.Q3, Quarter.Q4.minus(i));
+		}
+	}
+}
diff --git a/hutool-core/src/test/java/cn/hutool/core/date/YearQuarterTest.java b/hutool-core/src/test/java/cn/hutool/core/date/YearQuarterTest.java
new file mode 100644
index 000000000..441243116
--- /dev/null
+++ b/hutool-core/src/test/java/cn/hutool/core/date/YearQuarterTest.java
@@ -0,0 +1,1148 @@
+package cn.hutool.core.date;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.DateTimeException;
+import java.time.LocalDate;
+import java.time.Year;
+import java.time.YearMonth;
+import java.util.Calendar;
+import java.util.Date;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class YearQuarterTest {
+
+	// ================================
+	// #region - of(int year, int quarter)
+	// ================================
+
+	@ParameterizedTest
+	@ValueSource(ints = { 1, 2, 3, 4 })
+	void of_ValidYearAndQuarterValue_CreatesYearQuarter(int quarter) {
+		{
+			int year = 2024;
+			YearQuarter yearQuarter = YearQuarter.of(year, quarter);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(Quarter.of(quarter), yearQuarter.getQuarter());
+			assertEquals(quarter, yearQuarter.getQuarterValue());
+		}
+		{
+			int year = Year.MIN_VALUE;
+			YearQuarter yearQuarter = YearQuarter.of(year, quarter);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(Quarter.of(quarter), yearQuarter.getQuarter());
+			assertEquals(quarter, yearQuarter.getQuarterValue());
+		}
+		{
+			int year = Year.MAX_VALUE;
+			YearQuarter yearQuarter = YearQuarter.of(year, quarter);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(Quarter.of(quarter), yearQuarter.getQuarter());
+			assertEquals(quarter, yearQuarter.getQuarterValue());
+		}
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { -1, 0, 5, 108 })
+	void of_ValidYearAndInvalidQuarterValue_DateTimeException(int quarter) {
+		int year = 2024;
+		assertThrows(DateTimeException.class, () -> {
+			YearQuarter.of(year, quarter);
+		});
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { Year.MIN_VALUE - 1, Year.MAX_VALUE + 1 })
+	void of_InvalidYearAndValidQuarterValue_DateTimeException(int year) {
+		assertThrows(DateTimeException.class, () -> {
+			YearQuarter.of(year, 1);
+		});
+		assertThrows(DateTimeException.class, () -> {
+			YearQuarter.of(year, 2);
+		});
+		assertThrows(DateTimeException.class, () -> {
+			YearQuarter.of(year, 3);
+		});
+		assertThrows(DateTimeException.class, () -> {
+			YearQuarter.of(year, 4);
+		});
+	}
+
+	@Test
+	void of_InvalidYearAndInvalidQuarterValue_DateTimeException() {
+		final int[] years = { Year.MIN_VALUE - 1, Year.MAX_VALUE + 1 };
+		final int[] quarters = { -1, 0, 5, 108 };
+		for (int year : years) {
+			final int yearValue = year;
+			for (int quarter : quarters) {
+				final int quarterValue = quarter;
+				assertThrows(DateTimeException.class,
+						() -> YearQuarter.of(yearValue, quarterValue));
+			}
+		}
+	}
+
+	// ================================
+	// #endregion - of(int year, int quarter)
+	// ================================
+
+	// ================================
+	// #region - of(int year, Quarter quarter)
+	// ================================
+
+	@ParameterizedTest
+	@ValueSource(ints = { 1, 2, 3, 4 })
+	void of_ValidYearAndQuarter_CreatesYearQuarter(int quarterValue) {
+		{
+			int year = 2024;
+			Quarter quarter = Quarter.of(quarterValue);
+			YearQuarter yearQuarter = YearQuarter.of(year, quarter);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(quarter, yearQuarter.getQuarter());
+			assertEquals(quarterValue, yearQuarter.getQuarterValue());
+		}
+		{
+			int year = Year.MIN_VALUE;
+			Quarter quarter = Quarter.of(quarterValue);
+			YearQuarter yearQuarter = YearQuarter.of(year, quarter);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(quarter, yearQuarter.getQuarter());
+			assertEquals(quarterValue, yearQuarter.getQuarterValue());
+		}
+		{
+			int year = Year.MAX_VALUE;
+			Quarter quarter = Quarter.of(quarterValue);
+			YearQuarter yearQuarter = YearQuarter.of(year, quarter);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(quarter, yearQuarter.getQuarter());
+			assertEquals(quarterValue, yearQuarter.getQuarterValue());
+		}
+	}
+
+	@Test
+	void of_ValidYearAndNullQuarter_NullPointerException() {
+		int year = 2024;
+		assertThrows(NullPointerException.class, () -> {
+			YearQuarter.of(year, null);
+		});
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { Year.MIN_VALUE - 1, Year.MAX_VALUE + 1 })
+	void of_InvalidYearAndValidQuarter_DateTimeException(int year) {
+		assertThrows(DateTimeException.class, () -> {
+			YearQuarter.of(year, Quarter.Q1);
+		});
+		assertThrows(DateTimeException.class, () -> {
+			YearQuarter.of(year, Quarter.Q2);
+		});
+		assertThrows(DateTimeException.class, () -> {
+			YearQuarter.of(year, Quarter.Q3);
+		});
+		assertThrows(DateTimeException.class, () -> {
+			YearQuarter.of(year, Quarter.Q4);
+		});
+	}
+
+	@Test
+	void of_InvalidYearAndNullQuarter_DateTimeException() {
+		final int[] years = { Year.MIN_VALUE - 1, Year.MAX_VALUE + 1 };
+		for (int year : years) {
+			final int yearValue = year;
+			assertThrows(DateTimeException.class,
+					() -> YearQuarter.of(yearValue, null));
+
+		}
+	}
+
+	// ================================
+	// #endregion - of(int year, Quarter quarter)
+	// ================================
+
+	// ================================
+	// #region - of(LocalDate date)
+	// ================================
+
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			Year.MIN_VALUE,
+			Year.MAX_VALUE,
+	})
+	void of_ValidLocalDate_CreatesYearQuarter_Q1(int year) {
+		{
+			LocalDate date = YearMonth.of(year, 1).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(1, yq.getQuarterValue());
+			assertSame(Quarter.Q1, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 1).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(1, yq.getQuarterValue());
+			assertSame(Quarter.Q1, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 2).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(1, yq.getQuarterValue());
+			assertSame(Quarter.Q1, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 2).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(1, yq.getQuarterValue());
+			assertSame(Quarter.Q1, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 3).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(1, yq.getQuarterValue());
+			assertSame(Quarter.Q1, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 3).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(1, yq.getQuarterValue());
+			assertSame(Quarter.Q1, yq.getQuarter());
+		}
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			Year.MIN_VALUE,
+			Year.MAX_VALUE,
+	})
+	void of_ValidLocalDate_CreatesYearQuarter_Q2(int year) {
+		{
+			LocalDate date = YearMonth.of(year, 4).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(2, yq.getQuarterValue());
+			assertSame(Quarter.Q2, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 4).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(2, yq.getQuarterValue());
+			assertSame(Quarter.Q2, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 5).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(2, yq.getQuarterValue());
+			assertSame(Quarter.Q2, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 5).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(2, yq.getQuarterValue());
+			assertSame(Quarter.Q2, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 6).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(2, yq.getQuarterValue());
+			assertSame(Quarter.Q2, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 6).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(2, yq.getQuarterValue());
+			assertSame(Quarter.Q2, yq.getQuarter());
+		}
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			Year.MIN_VALUE,
+			Year.MAX_VALUE,
+	})
+	void of_ValidLocalDate_CreatesYearQuarter_Q3(int year) {
+		{
+			LocalDate date = YearMonth.of(year, 7).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(3, yq.getQuarterValue());
+			assertSame(Quarter.Q3, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 7).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(3, yq.getQuarterValue());
+			assertSame(Quarter.Q3, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 8).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(3, yq.getQuarterValue());
+			assertSame(Quarter.Q3, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 8).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(3, yq.getQuarterValue());
+			assertSame(Quarter.Q3, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 9).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(3, yq.getQuarterValue());
+			assertSame(Quarter.Q3, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 9).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(3, yq.getQuarterValue());
+			assertSame(Quarter.Q3, yq.getQuarter());
+		}
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			Year.MIN_VALUE,
+			Year.MAX_VALUE,
+	})
+	void of_ValidLocalDate_CreatesYearQuarter_Q4(int year) {
+		{
+			LocalDate date = YearMonth.of(year, 10).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(4, yq.getQuarterValue());
+			assertSame(Quarter.Q4, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 10).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(4, yq.getQuarterValue());
+			assertSame(Quarter.Q4, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 11).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(4, yq.getQuarterValue());
+			assertSame(Quarter.Q4, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 11).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(4, yq.getQuarterValue());
+			assertSame(Quarter.Q4, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 12).atDay(1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(4, yq.getQuarterValue());
+			assertSame(Quarter.Q4, yq.getQuarter());
+		}
+		{
+			LocalDate date = YearMonth.of(year, 12).atEndOfMonth();
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(4, yq.getQuarterValue());
+			assertSame(Quarter.Q4, yq.getQuarter());
+		}
+	}
+
+	@Test
+	void of_NullLocalDate_NullPointerException() {
+		LocalDate date = null;
+		assertThrows(NullPointerException.class, () -> {
+			YearQuarter.of(date);
+		});
+	}
+
+	// ================================
+	// #endregion - of(LocalDate date)
+	// ================================
+
+	// ================================
+	// #region - of(Date date)
+	// ================================
+
+	@SuppressWarnings("deprecation")
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			1,
+			999999,
+	})
+	void of_ValidDate_CreatesYearQuarter(int year) {
+		{
+			Date date = new Date(year - 1900, 1 - 1, 1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(1, yq.getQuarterValue());
+			assertSame(Quarter.Q1, yq.getQuarter());
+		}
+		{
+			Date date = new Date(year - 1900, 3 - 1, 31, 23, 59, 59);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(1, yq.getQuarterValue());
+			assertSame(Quarter.Q1, yq.getQuarter());
+		}
+		{
+			Date date = new Date(year - 1900, 4 - 1, 1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(2, yq.getQuarterValue());
+			assertSame(Quarter.Q2, yq.getQuarter());
+		}
+		{
+			Date date = new Date(year - 1900, 6 - 1, 30, 23, 59, 59);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(2, yq.getQuarterValue());
+			assertSame(Quarter.Q2, yq.getQuarter());
+		}
+		{
+			Date date = new Date(year - 1900, 7 - 1, 1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(3, yq.getQuarterValue());
+			assertSame(Quarter.Q3, yq.getQuarter());
+		}
+		{
+			Date date = new Date(year - 1900, 9 - 1, 30, 23, 59, 59);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(3, yq.getQuarterValue());
+			assertSame(Quarter.Q3, yq.getQuarter());
+		}
+		{
+			Date date = new Date(year - 1900, 10 - 1, 1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(4, yq.getQuarterValue());
+			assertSame(Quarter.Q4, yq.getQuarter());
+		}
+		{
+			Date date = new Date(year - 1900, 12 - 1, 31, 23, 59, 59);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(4, yq.getQuarterValue());
+			assertSame(Quarter.Q4, yq.getQuarter());
+		}
+	}
+
+	@Test
+	void of_NullDate_NullPointerException() {
+		Date date = null;
+		assertThrows(NullPointerException.class, () -> {
+			YearQuarter.of(date);
+		});
+	}
+
+	// ================================
+	// #endregion - of(Date date)
+	// ================================
+
+	// ================================
+	// #region - of(Calendar date)
+	// ================================
+
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			1,
+			999999,
+	})
+	void of_ValidCalendar_CreatesYearQuarter(int year) {
+		Calendar date = Calendar.getInstance();
+		{
+			date.set(year, 1 - 1, 1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(1, yq.getQuarterValue());
+			assertSame(Quarter.Q1, yq.getQuarter());
+		}
+		{
+			date.set(year, 3 - 1, 31, 23, 59, 59);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(1, yq.getQuarterValue());
+			assertSame(Quarter.Q1, yq.getQuarter());
+		}
+		{
+			date.set(year, 4 - 1, 1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(2, yq.getQuarterValue());
+			assertSame(Quarter.Q2, yq.getQuarter());
+		}
+		{
+			date.set(year, 6 - 1, 30, 23, 59, 59);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(2, yq.getQuarterValue());
+			assertSame(Quarter.Q2, yq.getQuarter());
+		}
+		{
+			date.set(year, 7 - 1, 1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(3, yq.getQuarterValue());
+			assertSame(Quarter.Q3, yq.getQuarter());
+		}
+		{
+			date.set(year, 9 - 1, 30, 23, 59, 59);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(3, yq.getQuarterValue());
+			assertSame(Quarter.Q3, yq.getQuarter());
+		}
+		{
+			date.set(year, 10 - 1, 1);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(4, yq.getQuarterValue());
+			assertSame(Quarter.Q4, yq.getQuarter());
+		}
+		{
+			date.set(year, 12 - 1, 31, 23, 59, 59);
+			YearQuarter yq = YearQuarter.of(date);
+			assertEquals(year, yq.getYear());
+			assertEquals(4, yq.getQuarterValue());
+			assertSame(Quarter.Q4, yq.getQuarter());
+		}
+	}
+
+	@Test
+	void of_NullCalendar_NullPointerException() {
+		Calendar date = null;
+		assertThrows(NullPointerException.class, () -> {
+			YearQuarter.of(date);
+		});
+	}
+
+	// ================================
+	// #endregion - of(Calendar date)
+	// ================================
+
+	// ================================
+	// #region - of(YearMonth yearMonth)
+	// ================================
+
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			Year.MIN_VALUE,
+			Year.MAX_VALUE,
+	})
+	void of_ValidYearMonth_CreatesYearMonth_Q1(int year) {
+		{
+			YearMonth yearMonth = YearMonth.of(year, 1);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(1, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q1, yearQuarter.getQuarter());
+		}
+		{
+			YearMonth yearMonth = YearMonth.of(year, 2);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(1, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q1, yearQuarter.getQuarter());
+		}
+		{
+			YearMonth yearMonth = YearMonth.of(year, 3);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(1, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q1, yearQuarter.getQuarter());
+		}
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			Year.MIN_VALUE,
+			Year.MAX_VALUE,
+	})
+	void of_ValidYearMonth_CreatesYearMonth_Q2(int year) {
+		{
+			YearMonth yearMonth = YearMonth.of(year, 4);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(2, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q2, yearQuarter.getQuarter());
+		}
+		{
+			YearMonth yearMonth = YearMonth.of(year, 5);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(2, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q2, yearQuarter.getQuarter());
+		}
+		{
+			YearMonth yearMonth = YearMonth.of(year, 6);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(2, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q2, yearQuarter.getQuarter());
+		}
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			Year.MIN_VALUE,
+			Year.MAX_VALUE,
+	})
+	void of_ValidYearMonth_CreatesYearMonth_Q3(int year) {
+		{
+			YearMonth yearMonth = YearMonth.of(year, 7);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(3, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q3, yearQuarter.getQuarter());
+		}
+		{
+			YearMonth yearMonth = YearMonth.of(year, 8);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(3, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q3, yearQuarter.getQuarter());
+		}
+		{
+			YearMonth yearMonth = YearMonth.of(year, 9);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(3, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q3, yearQuarter.getQuarter());
+		}
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			Year.MIN_VALUE,
+			Year.MAX_VALUE,
+	})
+	void of_ValidYearMonth_CreatesYearMonth_Q4(int year) {
+		{
+			YearMonth yearMonth = YearMonth.of(year, 10);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(4, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q4, yearQuarter.getQuarter());
+		}
+		{
+			YearMonth yearMonth = YearMonth.of(year, 11);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(4, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q4, yearQuarter.getQuarter());
+		}
+		{
+			YearMonth yearMonth = YearMonth.of(year, 12);
+			YearQuarter yearQuarter = YearQuarter.of(yearMonth);
+			assertEquals(year, yearQuarter.getYear());
+			assertEquals(4, yearQuarter.getQuarterValue());
+			assertSame(Quarter.Q4, yearQuarter.getQuarter());
+		}
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = {
+			2023, // 非闰年
+			2024, // 闰年
+			Year.MIN_VALUE,
+			Year.MAX_VALUE,
+	})
+	void of_NullYearMonth_CreatesYearMonth_Q4(int year) {
+		YearMonth yearMonth = null;
+		assertThrows(NullPointerException.class,
+				() -> YearQuarter.of(yearMonth));
+	}
+
+	// ================================
+	// #endregion - of(YearMonth yearMonth)
+	// ================================
+
+	// ================================
+	// #region - firstDate & lastDate
+	// ================================
+
+	@ParameterizedTest
+	@ValueSource(ints = { 1949, 1990, 2000, 2008, 2023, 2024, Year.MIN_VALUE, Year.MAX_VALUE })
+	void test_getFirstDate_And_getLastDate(int year) {
+		{
+			final int quarterValue = 1;
+			YearQuarter yearQuarter = YearQuarter.of(year, quarterValue);
+
+			LocalDate expectedFirstDate = LocalDate.of(year, 1, 1);
+			LocalDate expectedLastDate = LocalDate.of(year, 3, 31);
+
+			assertEquals(expectedFirstDate, yearQuarter.firstDate());
+			assertEquals(expectedLastDate, yearQuarter.lastDate());
+		}
+		{
+			final int quarterValue = 2;
+			YearQuarter yearQuarter = YearQuarter.of(year, quarterValue);
+
+			LocalDate expectedFirstDate = LocalDate.of(year, 4, 1);
+			LocalDate expectedLastDate = LocalDate.of(year, 6, 30);
+
+			assertEquals(expectedFirstDate, yearQuarter.firstDate());
+			assertEquals(expectedLastDate, yearQuarter.lastDate());
+		}
+		{
+			final int quarterValue = 3;
+			YearQuarter yearQuarter = YearQuarter.of(year, quarterValue);
+
+			LocalDate expectedFirstDate = LocalDate.of(year, 7, 1);
+			LocalDate expectedLastDate = LocalDate.of(year, 9, 30);
+
+			assertEquals(expectedFirstDate, yearQuarter.firstDate());
+			assertEquals(expectedLastDate, yearQuarter.lastDate());
+		}
+		{
+			final int quarterValue = 4;
+			YearQuarter yearQuarter = YearQuarter.of(year, quarterValue);
+
+			LocalDate expectedFirstDate = LocalDate.of(year, 10, 1);
+			LocalDate expectedLastDate = LocalDate.of(year, 12, 31);
+
+			assertEquals(expectedFirstDate, yearQuarter.firstDate());
+			assertEquals(expectedLastDate, yearQuarter.lastDate());
+		}
+	}
+
+	// ================================
+	// #endregion - firstDate & lastDate
+	// ================================
+
+	// ================================
+	// #region - firstYearMonth & lastYearMonth
+	// ================================
+
+	@ParameterizedTest
+	@ValueSource(ints = { 1949, 1990, 2000, 2008, 2023, 2024, Year.MIN_VALUE, Year.MAX_VALUE })
+	void test_firstYearMonth_And_lastYearMonth(int year) {
+		YearQuarter yq;
+
+		yq = YearQuarter.of(year, Quarter.Q1);
+		assertEquals(YearMonth.of(year, 1), yq.firstYearMonth());
+		yq = YearQuarter.of(year, Quarter.Q2);
+		assertEquals(YearMonth.of(year, 4), yq.firstYearMonth());
+		yq = YearQuarter.of(year, Quarter.Q3);
+		assertEquals(YearMonth.of(year, 7), yq.firstYearMonth());
+		yq = YearQuarter.of(year, Quarter.Q4);
+		assertEquals(YearMonth.of(year, 10), yq.firstYearMonth());
+
+		yq = YearQuarter.of(year, Quarter.Q1);
+		assertEquals(YearMonth.of(year, 3), yq.lastYearMonth());
+		yq = YearQuarter.of(year, Quarter.Q2);
+		assertEquals(YearMonth.of(year, 6), yq.lastYearMonth());
+		yq = YearQuarter.of(year, Quarter.Q3);
+		assertEquals(YearMonth.of(year, 9), yq.lastYearMonth());
+		yq = YearQuarter.of(year, Quarter.Q4);
+		assertEquals(YearMonth.of(year, 12), yq.lastYearMonth());
+	}
+
+	// ================================
+	// #endregion - firstYearMonth & lastYearMonth
+	// ================================
+
+	// ================================
+	// #region - firstMonth & lastMonth
+	// ================================
+
+	@ParameterizedTest
+	@ValueSource(ints = { 1949, 1990, 2000, 2008, 2023, 2024, Year.MIN_VALUE, Year.MAX_VALUE })
+	void testFirstMonthAndLastMonth(int year) {
+		YearQuarter q1 = YearQuarter.of(year, 1);
+		assertEquals(1, q1.firstMonthValue());
+		assertEquals(Month.JANUARY, q1.firstMonth());
+		assertEquals(3, q1.lastMonthValue());
+		assertEquals(Month.MARCH, q1.lastMonth());
+
+		YearQuarter q2 = YearQuarter.of(year, 2);
+		assertEquals(4, q2.firstMonthValue());
+		assertEquals(Month.APRIL, q2.firstMonth());
+		assertEquals(6, q2.lastMonthValue());
+		assertEquals(Month.JUNE, q2.lastMonth());
+
+		YearQuarter q3 = YearQuarter.of(year, 3);
+		assertEquals(7, q3.firstMonthValue());
+		assertEquals(Month.JULY, q3.firstMonth());
+		assertEquals(9, q3.lastMonthValue());
+		assertEquals(Month.SEPTEMBER, q3.lastMonth());
+
+		YearQuarter q4 = YearQuarter.of(year, 4);
+		assertEquals(10, q4.firstMonthValue());
+		assertEquals(Month.OCTOBER, q4.firstMonth());
+		assertEquals(12, q4.lastMonthValue());
+		assertEquals(Month.DECEMBER, q4.lastMonth());
+	}
+
+	// ================================
+	// #endregion - firstMonth & lastMonth
+	// ================================
+
+	// ================================
+	// #region - compareTo
+	// ================================
+
+	@Test
+	void testCompareTo() {
+		int year1;
+		int quarter1;
+		YearQuarter yearQuarter1;
+
+		year1 = 2024;
+		quarter1 = 1;
+		yearQuarter1 = YearQuarter.of(year1, Quarter.of(quarter1));
+
+		for (int year2 = 2000; year2 <= 2050; year2++) {
+			for (int quarter2 = 1; quarter2 <= 4; quarter2++) {
+				YearQuarter yearQuarter2 = YearQuarter.of(year2, Quarter.of(quarter2));
+
+				if (year1 == year2) {
+					// 同年
+					assertEquals(quarter1 - quarter2, yearQuarter1.compareTo(yearQuarter2));
+
+					if (quarter1 == quarter2) {
+						// 同年同季度
+						assertEquals(yearQuarter1, yearQuarter2);
+						assertEquals(0, yearQuarter1.compareTo(yearQuarter2));
+					} else if (quarter1 < quarter2) {
+						assertNotEquals(yearQuarter1, yearQuarter2);
+						assertTrue(yearQuarter1.isBefore(yearQuarter2));
+						assertFalse(yearQuarter1.isAfter(yearQuarter2));
+						assertFalse(yearQuarter2.isBefore(yearQuarter1));
+						assertTrue(yearQuarter2.isAfter(yearQuarter1));
+					} else if (quarter1 > quarter2) {
+						assertNotEquals(yearQuarter1, yearQuarter2);
+						assertFalse(yearQuarter1.isBefore(yearQuarter2));
+						assertTrue(yearQuarter1.isAfter(yearQuarter2));
+						assertTrue(yearQuarter2.isBefore(yearQuarter1));
+						assertFalse(yearQuarter2.isAfter(yearQuarter1));
+					}
+				} else {
+					// 不同年
+					assertEquals(year1 - year2, yearQuarter1.compareTo(yearQuarter2));
+					assertNotEquals(0, yearQuarter1.compareTo(yearQuarter2));
+					if (year1 < year2) {
+						assertNotEquals(yearQuarter1, yearQuarter2);
+						assertTrue(yearQuarter1.isBefore(yearQuarter2));
+						assertFalse(yearQuarter1.isAfter(yearQuarter2));
+						assertFalse(yearQuarter2.isBefore(yearQuarter1));
+						assertTrue(yearQuarter2.isAfter(yearQuarter1));
+					} else if (year1 > year2) {
+						assertNotEquals(yearQuarter1, yearQuarter2);
+						assertFalse(yearQuarter1.isBefore(yearQuarter2));
+						assertTrue(yearQuarter1.isAfter(yearQuarter2));
+						assertTrue(yearQuarter2.isBefore(yearQuarter1));
+						assertFalse(yearQuarter2.isAfter(yearQuarter1));
+					}
+				}
+			}
+		}
+	}
+
+	// ================================
+	// #endregion - compareTo
+	// ================================
+
+	@ParameterizedTest
+	@ValueSource(ints = { Year.MIN_VALUE + 25, Year.MAX_VALUE - 25, -1, 0, 1, 1949, 1990, 2000, 2008, 2023, 2024 })
+	void testPlusQuartersAndMinusQuarters(int year) {
+		for (int quarter = 1; quarter <= 4; quarter++) {
+			YearQuarter yq1 = YearQuarter.of(year, quarter);
+			for (int quartersToAdd = -100; quartersToAdd <= 100; quartersToAdd++) {
+				YearQuarter plus = yq1.plusQuarters(quartersToAdd);
+				YearQuarter minus = yq1.minusQuarters(-quartersToAdd);
+				assertEquals(plus, minus);
+
+				// offset: 表示自 公元 0000年以来,经历了多少季度。所以 0 表示 -0001,Q4; 1 表示 0000 Q1
+				long offset = (year * 4L + quarter) + quartersToAdd;
+				if (offset > 0) {
+					assertEquals((offset - 1) / 4, plus.getYear());
+					assertEquals(((offset - 1) % 4) + 1, plus.getQuarterValue());
+				} else {
+					assertEquals((offset / 4 - 1), plus.getYear());
+					assertEquals((4 + offset % 4), plus.getQuarterValue());
+				}
+			}
+		}
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { Year.MIN_VALUE + 1, Year.MAX_VALUE - 1, -1, 0, 1, 1900, 1990, 2000, 2023, 2024 })
+	void test_nextQuarter_And_lastQuarter(int year) {
+		int quarter;
+
+		YearQuarter yq;
+		YearQuarter next;
+		YearQuarter last;
+
+		quarter = 1;
+		yq = YearQuarter.of(year, quarter);
+		next = yq.nextQuarter();
+		assertEquals(year, next.getYear());
+		assertEquals(2, next.getQuarterValue());
+		last = yq.lastQuarter();
+		assertEquals(year - 1, last.getYear());
+		assertEquals(4, last.getQuarterValue());
+
+		quarter = 2;
+		yq = YearQuarter.of(year, quarter);
+		next = yq.nextQuarter();
+		assertEquals(year, next.getYear());
+		assertEquals(3, next.getQuarterValue());
+		last = yq.lastQuarter();
+		assertEquals(year, last.getYear());
+		assertEquals(1, last.getQuarterValue());
+
+		quarter = 3;
+		yq = YearQuarter.of(year, quarter);
+		next = yq.nextQuarter();
+		assertEquals(year, next.getYear());
+		assertEquals(4, next.getQuarterValue());
+		last = yq.lastQuarter();
+		assertEquals(year, last.getYear());
+		assertEquals(2, last.getQuarterValue());
+
+		quarter = 4;
+		yq = YearQuarter.of(year, quarter);
+		next = yq.nextQuarter();
+		assertEquals(year + 1, next.getYear());
+		assertEquals(1, next.getQuarterValue());
+		last = yq.lastQuarter();
+		assertEquals(year, last.getYear());
+		assertEquals(3, last.getQuarterValue());
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { Year.MIN_VALUE + 100, Year.MAX_VALUE - 100, -1, 0, 1, 1949, 1990, 2000, 2008, 2023, 2024 })
+	void test_PlusYearsAndMinusYears(int year) {
+		for (int yearToAdd = -100; yearToAdd <= 100; yearToAdd++) {
+			YearQuarter q1 = YearQuarter.of(year, Quarter.Q1);
+			YearQuarter plus = q1.plusYears(yearToAdd);
+			assertEquals(year + yearToAdd, plus.getYear());
+			assertEquals(Quarter.Q1, plus.getQuarter());
+			YearQuarter minus = q1.minusYears(yearToAdd);
+			assertEquals(Quarter.Q1, minus.getQuarter());
+			assertEquals(year - yearToAdd, minus.getYear());
+
+			assertEquals(q1.plusYears(yearToAdd), q1.minusYears(-yearToAdd));
+		}
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { Year.MIN_VALUE + 1, Year.MAX_VALUE - 1, -1, 0, 1, 1900, 1990, 2000, 2023, 2024 })
+	void test_nextYear_And_lastYear(int year) {
+		int quarter;
+
+		YearQuarter yq;
+		YearQuarter next;
+		YearQuarter last;
+
+		quarter = 1;
+		yq = YearQuarter.of(year, quarter);
+		next = yq.nextYear();
+		assertSame(Quarter.Q1, yq.getQuarter());
+		assertEquals(year + 1, next.getYear());
+		assertSame(Quarter.Q1, next.getQuarter());
+		last = yq.lastYear();
+		assertEquals(year - 1, last.getYear());
+		assertSame(Quarter.Q1, last.getQuarter());
+
+		quarter = 2;
+		yq = YearQuarter.of(year, quarter);
+		next = yq.nextYear();
+		assertSame(Quarter.Q2, yq.getQuarter());
+		assertEquals(year + 1, next.getYear());
+		assertSame(Quarter.Q2, next.getQuarter());
+		last = yq.lastYear();
+		assertEquals(year - 1, last.getYear());
+		assertSame(Quarter.Q2, last.getQuarter());
+
+		quarter = 3;
+		yq = YearQuarter.of(year, quarter);
+		next = yq.nextYear();
+		assertSame(Quarter.Q3, yq.getQuarter());
+		assertEquals(year + 1, next.getYear());
+		assertSame(Quarter.Q3, next.getQuarter());
+		last = yq.lastYear();
+		assertEquals(year - 1, last.getYear());
+		assertSame(Quarter.Q3, last.getQuarter());
+
+		quarter = 4;
+		yq = YearQuarter.of(year, quarter);
+		next = yq.nextYear();
+		assertSame(Quarter.Q4, yq.getQuarter());
+		assertEquals(year + 1, next.getYear());
+		assertSame(Quarter.Q4, next.getQuarter());
+		last = yq.lastYear();
+		assertEquals(year - 1, last.getYear());
+		assertSame(Quarter.Q4, last.getQuarter());
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { -1, 0, 1, 1900, 2000, 2023, 2024, Year.MAX_VALUE, Year.MIN_VALUE })
+	void test_compareTo_sameYear(int year) {
+		YearQuarter yq1 = YearQuarter.of(year, 1);
+		YearQuarter yq2 = YearQuarter.of(year, 2);
+		YearQuarter yq3 = YearQuarter.of(year, 3);
+		YearQuarter yq4 = YearQuarter.of(year, 4);
+
+		assertTrue(yq1.equals(YearQuarter.of(year, Quarter.Q1))); // NOSONAR
+		assertTrue(yq1.compareTo(yq1) == 0); // NOSONAR
+		assertTrue(yq1.compareTo(yq2) < 0);
+		assertTrue(yq1.compareTo(yq3) < 0);
+		assertTrue(yq1.compareTo(yq4) < 0);
+
+		assertTrue(yq2.equals(YearQuarter.of(year, Quarter.Q2))); // NOSONAR
+		assertTrue(yq2.compareTo(yq1) > 0);
+		assertTrue(yq2.compareTo(yq2) == 0); // NOSONAR
+		assertTrue(yq2.compareTo(yq3) < 0);
+		assertTrue(yq2.compareTo(yq4) < 0);
+
+		assertTrue(yq3.equals(YearQuarter.of(year, Quarter.Q3))); // NOSONAR
+		assertTrue(yq3.compareTo(yq1) > 0);
+		assertTrue(yq3.compareTo(yq2) > 0);
+		assertTrue(yq3.compareTo(yq3) == 0); // NOSONAR
+		assertTrue(yq3.compareTo(yq4) < 0);
+
+		assertTrue(yq4.equals(YearQuarter.of(year, Quarter.Q4))); // NOSONAR
+		assertTrue(yq4.compareTo(yq1) > 0);
+		assertTrue(yq4.compareTo(yq2) > 0);
+		assertTrue(yq4.compareTo(yq3) > 0);
+		assertTrue(yq4.compareTo(yq4) == 0); // NOSONAR
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { -1, 0, 1, 1900, 2000, 2023, 2024, Year.MAX_VALUE, Year.MIN_VALUE })
+	void test_isBefore_sameYear(int year) {
+		YearQuarter yq1 = YearQuarter.of(year, 1);
+		YearQuarter yq2 = YearQuarter.of(year, 2);
+		YearQuarter yq3 = YearQuarter.of(year, 3);
+		YearQuarter yq4 = YearQuarter.of(year, 4);
+
+		assertFalse(yq1.isBefore(YearQuarter.of(year, Quarter.Q1)));
+		assertTrue(yq1.isBefore(yq2));
+		assertTrue(yq1.isBefore(yq3));
+		assertTrue(yq1.isBefore(yq4));
+
+		assertFalse(yq2.isBefore(yq1));
+		assertFalse(yq2.isBefore(YearQuarter.of(year, Quarter.Q2)));
+		assertTrue(yq2.isBefore(yq3));
+		assertTrue(yq2.isBefore(yq4));
+
+		assertFalse(yq3.isBefore(yq1));
+		assertFalse(yq3.isBefore(yq2));
+		assertFalse(yq3.isBefore(YearQuarter.of(year, Quarter.Q3)));
+		assertTrue(yq3.isBefore(yq4));
+
+		assertFalse(yq4.isBefore(yq1));
+		assertFalse(yq4.isBefore(yq2));
+		assertFalse(yq4.isBefore(yq3));
+		assertFalse(yq4.isBefore(YearQuarter.of(year, Quarter.Q4)));
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { -1, 0, 1, 1900, 2000, 2023, 2024, Year.MAX_VALUE, Year.MIN_VALUE })
+	void test_isAfter_sameYear(int year) {
+		YearQuarter yq1 = YearQuarter.of(year, 1);
+		YearQuarter yq2 = YearQuarter.of(year, 2);
+		YearQuarter yq3 = YearQuarter.of(year, 3);
+		YearQuarter yq4 = YearQuarter.of(year, 4);
+
+		assertFalse(yq1.isAfter(YearQuarter.of(year, Quarter.Q1)));
+		assertFalse(yq1.isAfter(yq2));
+		assertFalse(yq1.isAfter(yq3));
+		assertFalse(yq1.isAfter(yq4));
+
+		assertTrue(yq2.isAfter(yq1));
+		assertFalse(yq2.isAfter(YearQuarter.of(year, Quarter.Q2)));
+		assertFalse(yq2.isAfter(yq3));
+		assertFalse(yq2.isAfter(yq4));
+
+		assertTrue(yq3.isAfter(yq1));
+		assertTrue(yq3.isAfter(yq2));
+		assertFalse(yq3.isAfter(YearQuarter.of(year, Quarter.Q3)));
+		assertFalse(yq3.isAfter(yq4));
+
+		assertTrue(yq4.isAfter(yq1));
+		assertTrue(yq4.isAfter(yq2));
+		assertTrue(yq4.isAfter(yq3));
+		assertFalse(yq4.isAfter(YearQuarter.of(year, Quarter.Q4)));
+	}
+
+	@Test
+	void test_compareTo_null() {
+		YearQuarter yq = YearQuarter.of(2024, 4);
+		assertThrows(NullPointerException.class,
+				() -> yq.compareTo(null));
+		assertThrows(NullPointerException.class,
+				() -> yq.isBefore(null));
+		assertThrows(NullPointerException.class,
+				() -> yq.isAfter(null));
+		assertNotEquals(null, yq);
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { -1, 0, 1, 1900, 2000, 2023, 2024, Year.MAX_VALUE - 1, Year.MIN_VALUE + 1 })
+	void test_compareTo_differentYear(int year) {
+		for (int quarter1 = 1; quarter1 <= 4; quarter1++) {
+			YearQuarter yq = YearQuarter.of(year, quarter1);
+			for (int quarter2 = 1; quarter2 <= 4; quarter2++) {
+				// gt
+				assertTrue(yq.compareTo(YearQuarter.of(year + 1, quarter2)) < 0);
+				assertTrue(yq.isBefore(YearQuarter.of(year + 1, quarter2)));
+				assertTrue(YearQuarter.of(year + 1, quarter2).compareTo(yq) > 0);
+				assertTrue(YearQuarter.of(year + 1, quarter2).isAfter(yq));
+				// lt
+				assertTrue(yq.compareTo(YearQuarter.of(year - 1, quarter2)) > 0);
+				assertTrue(yq.isAfter(YearQuarter.of(year - 1, quarter2)));
+				assertTrue(YearQuarter.of(year - 1, quarter2).compareTo(yq) < 0);
+				assertTrue(YearQuarter.of(year - 1, quarter2).isBefore(yq));
+			}
+		}
+	}
+}
diff --git a/pom.xml b/pom.xml
index e5aea9f40..47033ae35 100755
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,12 @@
 			<version>${junit.version}</version>
 			<scope>test</scope>
 		</dependency>
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-params</artifactId>
+			<version>${junit.version}</version>
+			<scope>test</scope>
+		</dependency>
 		<dependency>
 			<groupId>org.projectlombok</groupId>
 			<artifactId>lombok</artifactId>