13. Difference between `yyyy` and `YYYY` Java date pattern. What is week-based-year?

2022-01-09 java

No matter what application you are working on, it probably somewhere formats dates as String. If you not sure what is the difference between yyyy and YYYY patterns you might have been surprised in the last week of 2021. Or you even not noticed that something is wrong, because from 2022 everything works fine again. Possible consequence - in my case a component starts rejecting notifications from external service.

The reason was wrong value of calculated sign hash. When I check signed String it turned out that date was formatted as 2022-12-28 instead of 2021-12-28. Question was: why I got 2022 year if there still was 2021? The component worked as follows: received date field was automatically parsed to Date object type, later it was formatted as String with DateTimeFormatter using YYYY-MM-dd pattern. To fix problem I just used yyyy-MM-dd pattern instead. Difference between YYYY and yyyy according to DateTimeFormatter documentation:

  • YYYY is “week-based-year”
  • yyyy is “year-of-era”

What means “week-based-year”? According to WeekFields Java documentation

A week is defined by:

  • The first day-of-week. For example, the ISO-8601 standard considers Monday to be the first day-of-week.
  • The minimal number of days in the first week. For example, the ISO-8601 standard counts the first week as needing at least 4 days.

The ISO-8601 definition, where a week starts on Monday and the first week has a minimum of 4 days.

For my Locale(pl-PL) Java uses ISO-8601 definitions:

There are several mutually equivalent and compatible descriptions of week 01:

  • the week with the starting year’s first Thursday in it (the formal ISO definition),
  • the week with 4 January in it,
  • the first week with the majority (four or more) of its days in the starting year, and
  • the week starting with the Monday in the period 29 December - 4 January.

According to this definition the first week of 2022 is week from 3rd January to 9th January.

December-of-2021

All the following assertions are true.

@Test
@DisplayName("Locale.PL uses WeekFields.ISO")
public void localePlUsesWeekFieldsIso() {
    DateTimeFormatter formatterPl = DateTimeFormatter.ofPattern("YYYY-MM-dd ww", Locale.forLanguageTag("pl-PL"));
    assertEquals(LocalDate.of(2021, 12, 25).format(formatterPl), "2021-12-25 51");
    assertEquals(LocalDate.of(2021, 12, 26).format(formatterPl), "2021-12-26 51");
    assertEquals(LocalDate.of(2021, 12, 27).format(formatterPl), "2021-12-27 52");
    assertEquals(LocalDate.of(2021, 12, 28).format(formatterPl), "2021-12-28 52");
    assertEquals(LocalDate.of(2021, 12, 29).format(formatterPl), "2021-12-29 52");
    assertEquals(LocalDate.of(2021, 12, 30).format(formatterPl), "2021-12-30 52");
    assertEquals(LocalDate.of(2021, 12, 31).format(formatterPl), "2021-12-31 52");
    assertEquals(LocalDate.of(2022, 1, 1).format(formatterPl), "2021-01-01 52");
    assertEquals(LocalDate.of(2022, 1, 2).format(formatterPl), "2021-01-02 52");
    assertEquals(LocalDate.of(2022, 1, 3).format(formatterPl), "2022-01-03 01");
}

Pattern YYYY-MM-dd ww prints week-based-year (YYYY), month-of-year (MM), day-of-month (dd) and week-of-week-based-year(ww). Pay attention that formatter with YYYY pattern prints 2021 year for the two first days of 2022: LocalDate.of(2022, 1, 1) and LocalDate.of(2022, 1, 2) because, according to ISO-8601 definition, these days belong to the last week of 2021 (52nd week of 2021). If you want get day-based-year (or year-of-era) use yyyy pattern instead of YYYY.

But if the above definition would be always true the component should reject notifications in the first days of 2022 not the last days of 2021. The catch is that ISO definition of week is not used for all Locales. For example if you use Locale.US and run above example, you will be surprised again. Because for Locale.US Java uses Sunday start definition of week. According to WeekFields Java documentation

The common definition of a week that starts on Sunday and the first week has a minimum of 1 day.

According to the above definition the first week is week from 26th December to 1st January. All the following assertions are true.

@Test
@DisplayName("Locale.US uses WeekFields.SUNDAY_START")
public void localeUsUsesWeekFieldsSundayStart() {
    DateTimeFormatter formatterUs = DateTimeFormatter.ofPattern("YYYY-MM-dd ww", Locale.US);
    assertEquals(LocalDate.of(2021, 12, 25).format(formatterUs), "2021-12-25 52");
    assertEquals(LocalDate.of(2021, 12, 26).format(formatterUs), "2022-12-26 01");
    assertEquals(LocalDate.of(2021, 12, 27).format(formatterUs), "2022-12-27 01");
    assertEquals(LocalDate.of(2021, 12, 28).format(formatterUs), "2022-12-28 01");
    assertEquals(LocalDate.of(2021, 12, 29).format(formatterUs), "2022-12-29 01");
    assertEquals(LocalDate.of(2021, 12, 30).format(formatterUs), "2022-12-30 01");
    assertEquals(LocalDate.of(2021, 12, 31).format(formatterUs), "2022-12-31 01");
    assertEquals(LocalDate.of(2022, 1, 1).format(formatterUs), "2022-01-01 01");
    assertEquals(LocalDate.of(2022, 1, 2).format(formatterUs), "2022-01-02 02");
    assertEquals(LocalDate.of(2022, 1, 3).format(formatterUs), "2022-01-03 02");
}

Formatter with YYYY pattern prints 2022 starting on 26th December because, according to Sunday start definition, these days belong to the first week of 2022.

In general, week in Java is defined by WeekFields class which encapsulates two mentioned earlier “week-defining” values:

  • The first day-of-week.
  • The minimal number of days in the first week.

If you are interesting which WeekFields is used for your Locale, you can check it easily. The following code prints all possible WeekField definition with all assigned to it Locales.

public class PrintLocaleWeekFields {

    public static void main(String[] args) {
        Map<WeekFields, List<Locale>> locales = Arrays.stream(Locale.getAvailableLocales())
            .collect(Collectors.groupingBy(WeekFields::of));
        System.out.println(locales);
    }
}

Printed result:

WeekFields[MONDAY,1]=[tk_TM_#Latn, en_NU, ff_LR_#Adlm, es_BO, bs_BA, en_LR, ar_TD, nus_SS_#Latn, ff_MR_#Latn, sw_UG, tk_TM, sr_ME_#Cyrl, os_GE_#Cyrl, yo_NG, en_PW, sr_CS, agq_CM_#Latn, ar_EH, bs_BA_#Latn, dje_NE, hy_AM_#Armn, ff_GH_#Latn, fr_PM, ar_KM, agq_CM, tr_TR, kl_GL_#Latn, ar_MR, kl_GL, en_NR, rw_RW_#Latn, en_CY, tr_TR_#Latn, ti_ER, nus_SS, en_RW, hr_HR_#Latn, ln_CD, nnh_CM, dje_NE_#Latn, ksf_CM, fr_VU, nnh_CM_#Latn, fr_NE, bez_TZ_#Latn, ksb_TZ, ln_CF, en_CX, ak_GH_#Latn, en_TZ, fr_NC, fr_CM, pcm_NG_#Latn, teo_UG, ln_CG, az_AZ, el_CY, ku_TR, ff_SN, sq_MK, sr_BA_#Cyrl, so_SO_#Latn, tr_CY, lv_LV_#Latn, uz_UZ_#Latn, dua_CM, fr_TN, sr_RS, sw_TZ_#Latn, fr_PF, pt_GQ, vun_TZ, jmc_TZ, mg_MG_#Latn, en_TV, en_PN, lu_CD_#Latn, en_GY, dyo_SN, nl_CW, fr_GQ, en_NG, fr_CI, ia_001, en_LC, ff_BF_#Adlm, bm_ML_#Latn, yav_CM_#Latn, mk_MK, sl_SI, sg_CF_#Latn, jgo_CM, ff_NE_#Adlm, en_BM, kea_CV, vi_VN, mfe_MU, fr_BF, fr_YT, ff_CM_#Latn, sr_BA_#Latn, uk_UA_#Cyrl, ff_GW_#Adlm, ha_GH, yi_001_#Hebr, to_TO_#Latn, ff_GW_#Latn, mua_CM_#Latn, nyn_UG, ms_MY, rn_BI_#Latn, ta_LK, tg_TJ, vun_TZ_#Latn, es_EC, mk_MK_#Cyrl, ff_CM_#Adlm, lg_UG, ff_NE_#Latn, cgg_UG, pcm_NG, en_BI, mi_NZ, ar_ER, es_EA, fr_SC, en_SL, ff_NG_#Latn, ff_NG_#Adlm, en_SH, eo_001, en_SI, vai_LR_#Vaii, rof_TZ_#Latn, ar_LB, ff_GN, eo_001_#Latn, hr_HR, rof_TZ, mn_MN, en_FM, fr_WF, ff_GM_#Latn, teo_UG_#Latn, asa_TZ_#Latn, bez_TZ, ff_GN_#Latn, sl_SI_#Latn, en_FK, bas_CM_#Latn, en_DG, pt_ST, ak_GH, es_419, ln_CD_#Latn, kkj_CM_#Latn, es_IC, ar_TN, bm_ML, jmc_TZ_#Latn, khq_ML, en_SB, rw_RW, shi_MA_#Tfng, ro_MD, uz_UZ, ia_001_#Latn, en_SC, en_UG, en_NZ, es_UY, ru_UA, sg_CF, en_BB, hr_BA, yo_NG_#Latn, lu_CD, ar_001, so_SO, lv_LV, sr_RS_#Cyrl, en_LS, ka_GE, sw_TZ, fr_RW, mg_MG, sr_RS_#Latn, ky_KG, tzm_MA_#Latn, ku_TR_#Latn, mfe_MU_#Latn, ky_KG_#Cyrl, qu_EC, ka_GE_#Geor, en_MS, kde_TZ_#Latn, sr_ME, en_ZM, fr_ML, ha_NG, os_GE, yi_001, en_GH, tzm_MA, ses_ML, rwk_TZ_#Latn, vai_LR_#Latn, sw_CD, ff_MR_#Adlm, en_VC, ses_ML_#Latn, en_150, ha_NE, en_KN, ro_RO, sr_ME_#Latn, ff_LR_#Latn, bas_CM, fr_MG, es_CL, sq_AL, ro_RO_#Latn, twq_NE, nmg_CM, en_MP, en_GD, sbp_TZ_#Latn, kde_TZ, ta_MY, si_LK_#Sinh, en_KI, twq_NE_#Latn, sq_AL_#Latn, ff_GH_#Adlm, fr_MF, en_SZ, rwk_TZ, kk_KZ, ar_PS, kkj_CM, es_GQ, en_SX, ru_KZ, ko_KP, nl_SR, bem_ZM_#Latn, cgg_UG_#Latn, nl_BQ, ee_GH_#Latn, ff_GN_#Adlm, uz_UZ_#Cyrl, asa_TZ, fr_SN, fr_MA, ff_GM_#Adlm, fr_BL, mgo_CM, nmg_CM_#Latn, tg_TJ_#Cyrl, en_SS, shi_MA, en_MG, fr_BI, naq_NA_#Latn, fr_BJ, vai_LR, bs_BA_#Cyrl, khq_ML_#Latn, mn_MN_#Cyrl, wo_SN, ha_NG_#Latn, fr_HT, nl_SX, fr_CG, ms_MY_#Latn, zgh_MA_#Tfng, nyn_UG_#Latn, en_VU, to_TO, ff_SL_#Latn, xog_UG_#Latn, ff_SN_#Adlm, vi_VN_#Latn, jgo_CM_#Latn, zgh_MA, uk_UA, lg_UG_#Latn, en_NF, sr_XK_#Cyrl, ar_SS, nl_AW, en_AI, xog_UG, en_CM, ru_MD, ff_SN_#Latn, en_TO, ff_SL_#Adlm, en_PG, fr_CF, pt_TL, en_ER, sr_BA, be_BY_#Cyrl, fr_TG, sr_XK_#Latn, ig_NG, fr_GN, en_CK, ar_MA, fr_TD, bem_ZM, ewo_CM_#Latn, ewo_CM, fr_CD, rn_BI, en_NA, mgo_CM_#Latn, lag_TZ, qu_BO, kea_CV_#Latn, sq_XK, mi_NZ_#Latn, en_KY, si_LK, ar_SO, fr_MU, en_TK, mua_CM, pt_GW, ee_TG, ln_AO, be_BY, pt_CV, ru_BY, ee_GH, kk_KZ_#Cyrl, yo_BJ, fr_KM, es_AR, en_MY, sbp_TZ, hy_AM, en_GM, ksb_TZ_#Latn, pt_AO, en_001, dua_CM_#Latn, lag_TZ_#Latn, ru_KG, fr_MR, ksf_CM_#Latn, ff_BF_#Latn, en_MW, naq_NA, en_IO, en_CC, az_AZ_#Cyrl, shi_MA_#Latn, es_CU, ig_NG_#Latn, yav_CM, da_GL, wo_SN_#Latn, es_CR, fr_GA, en_MU, az_AZ_#Latn, dyo_SN_#Latn, en_VG, en_TC, af_NA, mas_TZ, ms_BN],
WeekFields[SATURDAY,1]=[ar_EG, ps_AF_#Arab, ar_SY, uz_AF, lrc_IR, ar_OM, lrc_IQ, ar_DZ, fa_IR_#Arab, fr_DJ, so_DJ, lrc_IR_#Arab, ar_AE, mzn_IR, en_SD, uz_AF_#Arab, ar_IQ, ar_KW, ar_JO, fr_DZ, fa_AF, ckb_IQ_#Arab, ar_SD, fa_IR, kab_DZ_#Latn, kab_DZ, ps_AF, mzn_IR_#Arab, en_AE, ar_BH, ar_QA, ar_EG_#Arab, ckb_IQ, ckb_IR, ar_LY, fr_SY, ar_DJ],
WeekFields[MONDAY,4]=[se_NO_#Latn, dsb_DE, lb_LU_#Latn, se_NO, pl_PL, nds_DE, nb_SJ, lb_LU, dsb_DE_#Latn, is_IS_#Latn, no_NO_NY, pl_PL_#Latn, nds_DE_#Latn, tt_RU, fur_IT_#Latn, ast_ES_#Latn, en_JE, da_DK_#Latn, en_AT, gd_GB, wae_CH_#Latn, no_NO_#Latn, smn_FI_#Latn, en_NL, gsw_FR, hu_HU, bg_BG_#Cyrl, et_EE, fy_NL, de_IT, de_CH, nl_NL, pt_CH, gv_IM, kw_GB_#Latn, fi_FI_#Latn, fr_BE, nb_NO, it_SM, fi_FI, ca_FR, de_BE, de_DE_#Latn, fr_FR_#Latn, ksh_DE, ru_RU, ga_IE, rm_CH_#Latn, fr_LU, ga_GB, no_NO, de_LU, de_DE, nn_NO_#Latn, en_DK, lt_LT, ksh_DE_#Latn, da_DK, en_BE, tt_RU_#Cyrl, ga_IE_#Latn, is_IS, en_SE, gsw_LI, kw_GB, en_DE, en_FI, sah_RU_#Cyrl, en_FJ, de_LI, smn_FI, de_AT, ce_RU, os_RU, nl_NL_#Latn, gl_ES_#Latn, en_GG, sv_SE, el_GR_#Grek, it_VA, es_ES, bg_BG, hsb_DE, eu_ES_#Latn, sv_SE_#Latn, sv_FI, en_IE, fo_FO_#Latn, en_GB, fr_MC, ce_RU_#Cyrl, gsw_CH, pt_LU, hsb_DE_#Latn, br_FR_#Latn, es_ES_#Latn, cy_GB_#Latn, fr_FR, sah_RU, sk_SK, ru_RU_#Cyrl, gv_IM_#Latn, nds_NL, fr_RE, fr_GP, fr_CH, nb_NO_#Latn, fy_NL_#Latn, cs_CZ, ca_ES, hu_HU_#Latn, rm_CH, et_EE_#Latn, gd_GB_#Latn, se_FI, lt_LT_#Latn, ca_IT, ca_AD, it_CH, it_IT, nn_NO, se_SE, it_IT_#Latn, wae_CH, fo_DK, en_CH, fo_FO, ast_ES, fr_MQ, fur_IT, fr_GF, sk_SK_#Latn, eu_ES, el_GR, ca_ES_VALENCIA, gl_ES, en_IM, gsw_CH_#Latn, en_GI, ca_ES_#Latn, nl_BE, cy_GB, sv_AX, cs_CZ_#Latn, br_FR],
WeekFields[SUNDAY,1]=[, he, th_TH_#Thai, nds, ti_ET, ta_SG, lv, zh_SG_#Hans, en_JM, kkj, sd__#Arab, dz_BT, mni, yi, cs, el, af, smn, dsb, khq, ne_IN, es_US, sa, en_US_POSIX, pt_MO, zh__#Hans, so_KE, gu_IN_#Gujr, teo, eu, es_DO, ru, az, su__#Latn, fa, nd, kk, hy, en_AU, ksb, luo, lb, su, no, ar_IL, mgh, or_IN, az__#Latn, ta, lag, luo_KE_#Latn, bo, om_KE, en_AS, zh_TW, sd_IN, kln, mai, pt_MZ, my_MM_#Mymr, gl, sr__#Cyrl, yue_CN_#Hans, ff__#Adlm, kn_IN, ga, qu, en_PR, mua, jv, ps, sn, km, zgh, es, jgo, gsw, pa_IN_#Guru, ur_PK_#Arab, ceb, bn_BD_#Beng, ne_NP_#Deva, te, sl, mr_IN, ha, guz_KE_#Latn, es_HN, sbp, sw, nmg, pt_BR_#Latn, vai__#Vaii, gu, lo, zh_HK_#Hans, bs__#Latn, os, am, ki_KE, en_PK, zh_CN, rw, brx_IN, en_TT, dav, ses, xh_ZA, es_VE, mer_KE, mg, mr, seh, mgo, en_US, pa__#Guru, sa_IN_#Deva, gu_IN, ast, mt_MT_#Latn, yue__#Hans, ccp_BD_#Cakm, ks__#Arab, af_ZA_#Latn, ti_ET_#Ethi, am_ET_#Ethi, cgg, zh_MO, ksf, cy, ceb_PH, sq, fr, qu_PE, de, zu_ZA, su_ID_#Latn, lg, en_DM, sd, he_IL_#Hebr, yue_CN, en_WS, so, kab, nus, sn_ZW, th_TH_TH_#u-nu-thai, hi, zh_MO_#Hant, vai, sd_PK_#Arab, mi, mt, yav, kam, ro, ps_PK, ee, en_UM, lo_LA, chr, af_ZA, doi, es_BZ, as, it, ks_IN, my_MM, ur_PK, ii, naq, en_SG, kln_KE, tzm, fur, om, mai_IN_#Deva, ja_JP_JP_#u-ca-japanese, es_SV, pt_BR, mni_IN_#Beng, ml_IN, hr, lt, ccp, zh_CN_#Hans, en, guz_KE, ccp_BD, ca, pa_PK, ug_CN, ki_KE_#Latn, es_BR, bo_CN_#Tibt, chr_US, nyn, mk, sat, pa__#Arab, bs, fy, th, dav_KE, dje, mas_KE, mni_IN, ckb, bem, da, wae, ig, en_HK, brx_IN_#Deva, mer_KE_#Latn, en_US_#Latn, ki, nb, kok, ewo, nn, bg, kea, zu, am_ET, bo_CN, hsb, kok_IN_#Deva, sat_IN, pcm, sah, mer, br, ar_SA, fil_PH_#Latn, sk, om_ET_#Latn, ml, en_MT, en_IL, sv, kn_IN_#Knda, lkt_US, sd__#Deva, ku, fil_PH, es_PH, es_CO, agq, ebu, es_GT, nd_ZW_#Latn, mn, kam_KE, en_MO, ja_JP_#Jpan, wo, shi__#Tfng, en_BZ, lkt_US_#Latn, ta_IN_#Taml, az__#Cyrl, tk, shi__#Latn, en_BW, he_IL, nd_ZW, luy_KE_#Latn, mni__#Beng, ne, zh_SG, om_ET, lo_LA_#Laoo, ja_JP, kam_KE_#Latn, my, ka, ko_KR_#Kore, ms_ID, shi, kl, sa_IN, yue_HK, id, zh, es_PE, mgh_MZ, saq, zh_HK_#Hant, sat_IN_#Olck, es_PA, bez, kw, vai__#Latn, ksh, ur_IN, ln, luy_KE, pt, mgh_MZ_#Latn, ar_YE, to, et, rof, en_BS, be, gv, kln_KE_#Latn, dua, hi_IN_#Deva, guz, en_KE, mfe, ja, or, brx, mai_IN, ko_KR, es_MX, zu_ZA_#Latn, doi_IN, fi, uz, bs__#Cyrl, sr__#Latn, bo_IN, rm, bn, kn, nnh, bn_BD, en_ZA, pa_IN, en_MH, zh__#Hant, jv_ID_#Latn, ky, mas, xh_ZA_#Latn, dav_KE_#Latn, xh, te_IN, mas_KE_#Latn, lrc, ce, mt_MT, ko, ml_IN_#Mlym, ak, kde, dz, ia, seh_MZ, su_ID, ii_CN, pa_PK_#Arab, bn_IN, pa, rwk, rn, tg, hu, ceb_PH_#Latn, twq, bm, en_GU, tr, es_PY, kok_IN, dz_BT_#Tibt, en_PH, zh_MO_#Hans, sat__#Olck, ff, haw_US_#Latn, en_AG, ebu_KE, xog, ms, ug, qu_PE_#Latn, id_ID, teo_KE, haw_US, vi, fr_CA, dyo, luo_KE, eo, en_ZW, pl, ur, uz__#Arab, saq_KE, se, sn_ZW_#Latn, ms_SG, yue__#Hant, km_KH_#Khmr, luy, uk, es_PR, mzn, tt, ug_CN_#Arab, hi_IN, saq_KE_#Latn, asa, ff__#Latn, doi_IN_#Deva, ebu_KE_#Latn, uz__#Cyrl, fil, yue_HK_#Hant, fo, ne_NP, ta_IN, lkt, id_ID_#Latn, is, te_IN_#Telu, si, jv_ID, ks, zh_TW_#Hant, as_IN, zh_HK, sw_KE, th_TH, as_IN_#Beng, jmc, yue, ar, en_VI, haw, bas, uz__#Latn, sg, km_KH, nl, ks_IN_#Arab, sd_IN_#Deva, mr_IN_#Deva, sr, seh_MZ_#Latn, en_CA, gd, chr_US_#Cher, or_IN_#Orya, so_ET, vun, en_IN, lu, yo, es_NI, ii_CN_#Yiii, sd_PK, ti, ccp_IN],
WeekFields[SUNDAY,4]=[pt_PT]}

All code examples from this post you will find at my github