Skip to content

ICU4J日历类解读

ICU4J日历类解读

com.ibm.icu.util.Calendar

1、日历类的定义

Calendar是一个抽象基类,用于在Date对象和一组整数字段(如YEAR、MONTH、DAY、HOUR等)之间进行转换。(Date对象是毫秒精度表示特定的时间瞬间)

ICU4J包含几个实现不同国际日历系统的子类。比如

  • buddhist 佛教历法
  • chinese 农历
  • japanese 日本历,也叫和历
  • Gregorian 格里高利历,也就是公历,或者说阳历
java
public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar>

2、日历类中的数据流

在日历中表示当前时间有2种方式:

  • 一种是时间戳,
  • 一种是当地的年月日时分秒的字段值(当然这种方式需要知道时区timezone或者utcOffset)。
java
private transient int fields[]; //当地的年月日时分秒的字段值数组
private long time; //时间戳

    //   local fields (YEAR, MONTH, DATE, HOUR, MINUTE, etc.)
    //           |
    //           | Using Calendar-specific algorithm
    //           V
    //   local standard millis
    //           |
    //           | Using TimeZone or user-set ZONE_OFFSET / DST_OFFSET
    //           V
    //   UTC millis (in time data member)

3、日历类的构造函数

java
public static Calendar getInstance(TimeZone zone, ULocale locale) {
    return getInstanceInternal(zone, locale);
}
private static Calendar getInstanceInternal(TimeZone tz, ULocale locale) {
    if (locale == null) {
        locale = ULocale.getDefault(Category.FORMAT);
    }
    if (tz == null) {
        tz = TimeZone.getDefault();
    }

    Calendar cal = createInstance(locale);
    cal.setTimeZone(tz);
    cal.setTimeInMillis(System.currentTimeMillis());
    return cal;
}

入参是时区和locale信息(没传的话使用默认值)

默认行为:

  • 设置时间戳为当前时间
  • 默认使用公历

4、核心字段

java
//日期相关的字段
public final static int ERA = 0;
public final static int YEAR = 1;
public final static int MONTH = 2;
public final static int WEEK_OF_YEAR = 3;
public final static int WEEK_OF_MONTH = 4;
public final static int DATE = 5;   //和DAY_OF_MONTH一样
public final static int DAY_OF_MONTH = 5;
public final static int DAY_OF_YEAR = 6;
public final static int DAY_OF_WEEK = 7;
public final static int DAY_OF_WEEK_IN_MONTH = 8;

//时间相关的字段
public final static int AM_PM = 9;
public final static int HOUR = 10;
public final static int HOUR_OF_DAY = 11;
public final static int MINUTE = 12;
public final static int SECOND = 13;
public final static int MILLISECOND = 14;

5、核心方法

  • set(), add(), and roll()这三个方法用来更改日历字段值
  • get(int field)用来获取日历值

5.1、核心方法1:set

set(int field, int value) 将字段f设置为value

此外,它还设置一个内部成员变量,以指示字段f已更改。

尽管字段f会立即更改,但在下一次调用get()、getTime()或getTimeInMillis()之前,不会重新计算日历的毫秒数。

因此,多次调用set()不会触发多次不必要的计算。

由于使用set()更改字段,其他字段也可能会更改,具体取决于字段、字段值和日历系统。

示例:比如日历最初设置为1999年8月31日。调用set(Calendar.MONTH,Calendar.SEPTEMBER)将日历设置为1999年9月31日。

这是一个临时的内部表示,如果随后调用getTime(),则解析为1999年10月1日。

但是,在调用getTime()之前调用set(Calendar.DAY_OF_MONTH,30)会将日历设置为1999年9月30日,因为set()本身之后不会进行重新计算。

java
public final void set(int field, int value)
{
    //默认先不计算
    if (areFieldsVirtuallySet) {
        computeFields();
    }
    //值设置相应的值
    fields[field] = value;
    /* Ensure that the fNextStamp value doesn't go pass max value for 32 bit integer */
    if (nextStamp == STAMP_MAX) {
        recalculateStamp();
    }
    stamp[field] = nextStamp++;
    //设置一个内部成员变量,以指示字段f已更改。
    isTimeSet = areFieldsSet = areFieldsVirtuallySet = false;
}

5.2、核心方法2:add

add(f,delta)将delta添加到字段f。这相当于调用set(f,get(f)+delta)并进行两次调整:

  • add规则1:调用后的字段f值减去调用前的字段f的值是delta,对字段f中发生的任何溢出会进行取模运算。 当字段值超出其范围时,会发生溢出,因此,下一个较大的字段会递增或递减,字段值会调整回其范围。
  • add规则2:由于字段f改变后,较小的字段其最小值或最大值可能发生变化,因此它可能会调整,会将其值调整为尽可能接近其期望值。比如加1个月,可能会导致DAY_OF_MONTH变化

此外,与set()不同,add()强制立即重新计算日历的毫秒和所有字段。

示例:考虑一个最初设置为1999年8月31日的公历GregorianLendar。调用add(Calendar.MONTH,13)将日历设置为2000年9月30日。 按照add规则1:将MONTH字段设置为9月,因为将8月加13个月,即为下一年的9月。由于在公历中DAY_OF_MONTH在9月份不能为31, 因此根据add规则2:将DAY_OF_MONTH设置为30,这是最接近的值。

go原生的addDate函数规则跟ICU不一致。它与规则2不一样,它会进行溢出。比如10月31号加一个月,先变成11.31(不存在),然后变成了12.1。 参考:https://pkg.go.dev/time#Time.AddDate

5.3、核心方法3:roll

roll(f, delta) adds delta to field f without changing larger fields.

  • Roll规则:Larger fields are unchanged after the call. A larger field represents a larger unit of time. DAY_OF_MONTH is a larger field than HOUR. 更大的字段不会被更改,比如+13小时,相当于是+1小时,不会改变天

5.5、核心方法4:get

java
//获取字段的值
public final int get(int field)
{
    complete(); //先计算
    return fields[field]; //再返回
}

6、使用模型

考虑一个用户界面组件,该组件包含月份、日期和年份的递增和递减按钮。

如果界面显示的是1999年1月31日,用户按下月份递增按钮,那么应该显示什么?

如果底层实现使用set(),它可能会读取1999年3月3日。

更好的结果是1999年2月28日。

此外,如果用户再次按下月份递增按钮,则应显示为1999年3月31日,而不是1999年3日28日。

通过保存原始日期并使用add()或roll()(取决于较大的字段是否会受到影响),用户界面可以按照大多数用户的直觉进行操作。

注意:您应该始终使用roll和add,而不是尝试直接对Calendar的字段执行算术运算。 日历子类很可能具有具有非线性行为的字段,例如在非闰年期间缺少月份或天数。子类的add和roll方法将考虑到这一点,而简单的算术操作可能会产生无效的结果。