首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Calendar 獲取上一季度起始與結束日期工具類

     編輯:Java 相關知識

Java 日期運算實戰:精確計算上一季度起止時間,解決月份邊界與時分秒陷阱

財務報表、業務統計、數據倉庫 ETL 常需按自然季度劃分時間範圍。本文提供基於 Calendar 的健壯實現,處理季度邊界、月末日期、時分秒歸零等細節,並給出 Java 8+ java.time 現代替代方案。

季度劃分規則

季度月份範圍起始日結束日
Q11-3 月1 月 1 日 00:00:003 月 31 日 23:59:59
Q24-6 月4 月 1 日 00:00:006 月 30 日 23:59:59
Q37-9 月7 月 1 日 00:00:009 月 30 日 23:59:59
Q410-12 月10 月 1 日 00:00:0012 月 31 日 23:59:59

Calendar 版實現 (兼容 JDK 6+)

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class QuarterUtils {
    private static final SimpleDateFormat FULL_FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private static final SimpleDateFormat DAY_FMT = new SimpleDateFormat("yyyy-MM-dd");

    /**
     * 獲取上一季度起始或結束時間
     * @param isStart true=季度首日 00:00:00, false=季度末日 23:59:59
     * @return 格式化時間字符串
     */
    public static String getLastQuarterTime(boolean isStart) {
        Calendar cal = Calendar.getInstance();
        int currentMonth = cal.get(Calendar.MONTH); // 0-based
        
        // 1. 確定當前季度首月 (0,3,6,9)
        int currentQuarterStartMonth;
        if (currentMonth <= 2) currentQuarterStartMonth = 0;      // Q1
        else if (currentMonth <= 5) currentQuarterStartMonth = 3; // Q2
        else if (currentMonth <= 8) currentQuarterStartMonth = 6; // Q3
        else currentQuarterStartMonth = 9;                        // Q4
        
        // 2. 回退到上一季度首月
        int lastQuarterStartMonth = currentQuarterStartMonth - 3;
        if (lastQuarterStartMonth < 0) {
            lastQuarterStartMonth += 12;
            cal.add(Calendar.YEAR, -1); // 跨年
        }
        
        // 3. 設置到上一季度首月 1 號
        cal.set(Calendar.MONTH, lastQuarterStartMonth);
        cal.set(Calendar.DATE, 1);
        
        if (isStart) {
            // 季度開始:首日 00:00:00
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
            cal.set(Calendar.MILLISECOND, 0);
        } else {
            // 季度結束:末月最後一日 23:59:59
            // 先加 3 個月到下一季度首月,再減 1 天
            cal.add(Calendar.MONTH, 3);
            cal.add(Calendar.DATE, -1);
            cal.set(Calendar.HOUR_OF_DAY, 23);
            cal.set(Calendar.MINUTE, 59);
            cal.set(Calendar.SECOND, 59);
            cal.set(Calendar.MILLISECOND, 999);
        }
        
        return FULL_FMT.format(cal.getTime());
    }
    
    // 測試
    public static void main(String[] args) {
        System.out.println("上季度開始: " + getLastQuarterTime(true));
        System.out.println("上季度結束: " + getLastQuarterTime(false));
    }
}

關鍵設計決策解析

  1. 不硬編碼月末日期:用 cal.add(MONTH, 3); cal.add(DATE, -1) 自動推導,自動處理 28/29/30/31 日及閏年
  2. 毫秒級精度:結束時間設為 23:59:59.999,配合資料庫 <= 查詢避免邊界遺漏
  3. 跨年處理:1 月查詢上季度需回退年份,代碼通用無特判
  4. 線程安全:方法內創建 Calendar 實例,無共享狀態

Java 8+ 現代寫法 (推薦新項目)

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;

public class QuarterUtilsModern {
    private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    public static String getLastQuarterStart() {
        LocalDateTime now = LocalDateTime.now();
        // 獲取當前季度首日
        LocalDateTime quarterStart = now.with(TemporalAdjusters.firstDayOfMonth())
                                        .withMonth(firstMonthOfQuarter(now.getMonthValue()))
                                        .withHour(0).withMinute(0).withSecond(0).withNano(0);
        // 減去一個季度
        return quarterStart.minusMonths(3).format(FMT);
    }
    
    public static String getLastQuarterEnd() {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime quarterStart = now.with(TemporalAdjusters.firstDayOfMonth())
                                        .withMonth(firstMonthOfQuarter(now.getMonthValue()))
                                        .withHour(0).withMinute(0).withSecond(0).withNano(0);
        // 上季度末 = 當前季度首日 - 1 納秒 (或 -1 秒視精度)
        return quarterStart.minusMonths(3).plusMonths(3).minusNanos(1).format(FMT);
        // 更清晰寫法:
        // return quarterStart.minusMonths(3).with(TemporalAdjusters.lastDayOfMonth())
        //                  .withHour(23).withMinute(59).withSecond(59).withNano(999_000_000).format(FMT);
    }
    
    private static int firstMonthOfQuarter(int month) {
        return ((month - 1) / 3) * 3 + 1; // 1,4,7,10
    }
    
    // 更優雅:使用 YearQuarter (Java 15+ 預覽) 或三方庫 ThreeTen-Extra
    // YearQuarter q = YearQuarter.from(LocalDate.now()).minus(1);
    // LocalDate start = q.atDay(1);
    // LocalDate end = q.atEndOfQuarter();

資料庫查詢最佳實踐

-- MySQL: 使用半開區間 [start, end) 避免秒級精度問題
SELECT * FROM orders 
WHERE create_time >= '2024-01-01 00:00:00' 
  AND create_time < '2024-04-01 00:00:00';

-- 索引友好:時間列加索引,避免函數運算
-- WHERE DATE(create_time) BETWEEN ... ❌ 不走索引
-- WHERE create_time >= ? AND create_time < ? ✅ 走索引

常見錯誤對照表

錯誤代碼問題後果修正
cal.set(MONTH, 2); cal.set(DATE, 31)硬編碼 3 月 31 日Q2 結束變成 6 月 31 日 (不存在)用 add(MONTH,3) + add(DATE,-1)
只設置 HOUR/MINUTE/SECOND忽略 MILLISECOND結束時間 23:59:59.000 漏掉 59.999 的數據設置 MILLISECOND=999
SimpleDateFormat 靜態共享非線程安全高併發下格式錯亂/異常ThreadLocal 或局部變量/DateTimeFormatter
跨年邏輯缺失1 月查詢上季度不減年份年份錯誤檢測月份回退時調整年份

擴展:獲取任意季度範圍

public static String[] getQuarterRange(int year, int quarter, boolean includeTime) {
    // quarter: 1-4
    Calendar cal = Calendar.getInstance();
    cal.set(Calendar.YEAR, year);
    cal.set(Calendar.MONTH, (quarter - 1) * 3); // 0,3,6,9
    cal.set(Calendar.DATE, 1);
    setTime(cal, true);  // 00:00:00
    String start = FULL_FMT.format(cal.getTime());
    
    cal.add(Calendar.MONTH, 3);
    cal.add(Calendar.DATE, -1);
    setTime(cal, false); // 23:59:59
    String end = FULL_FMT.format(cal.getTime());
    
    return new String[]{start, end};
}
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved