diff --git a/src/format/locales.rs b/src/format/locales.rs index f7b4bbde5b..dd563feb76 100644 --- a/src/format/locales.rs +++ b/src/format/locales.rs @@ -31,3 +31,115 @@ pub(crate) fn d_t_fmt(locale: Locale) -> &'static str { pub(crate) fn t_fmt(locale: Locale) -> &'static str { locale_match!(locale => LC_TIME::T_FMT) } + +macro_rules! expand_ampm { + ($item:ident) => { + pure_rust_locales::$item::LC_TIME::T_FMT_AMPM + }; +} + +/// Hardcoded `locale_match!` fallback for `T_FMT_AMPM`. +/// +/// `local_match_ampm` takes variable number of possible locales that have `LC_TIME::T_FMT_AMPM` field. +/// It will then match the locale and return its `LC_TIME::T_FMT_AMPM` field. +/// It will fallback to `en_US` if the field is an empty string. +/// +/// ```rust,ignore +/// // usage: +/// locale => ( Locale::POSIX, Locale::aa_DJ, ...) +/// // expands into: +/// match locale { +/// Locale::POSIX => { +/// let ampm = pure_rust_locales::POSIX::LC_TIME::T_FMT_AMPM; +/// +/// if ampm.is_empty() { pure_rust_locales::en_US::LC_TIME::T_FMT_AMPM } else { ampm } +/// }, +/// Locale::aa_DJ => // ... +/// // ... +/// _ => pure_rust_locales::en_US::LC_TIME::T_FMT_AMPM, +/// } +/// ``` +macro_rules! locale_match_ampm { + ($locale:expr => ( $($item:ident),* $(,)? )) => { + match $locale { + $(Locale::$item => { + let ampm = expand_ampm!($item); + + if ampm.is_empty() { expand_ampm!(en_US) } else { ampm } + },)* + + // default fallback is en_US + _ => expand_ampm!(en_US), + } + } +} + +/// Finds a locale's time format in 12 hour AM/PM. +/// +/// `t_fmt_ampm` uses [`locale_match_ampm`] macro to find `LC_TIME::T_FMT_AMPM` field in `locale`. +/// +/// ## Why `locale_match` will not work +/// +/// [locale_match] assumes every single locale module to have same field. +/// however, due to issues in [pure-rust-locales][Issue #4], +/// three locales (ff_SN, km_KH, ug_CN) are missing `LC_TIME::T_FMT_AMPM` field, +/// causing a compile error. +/// +/// example: +/// ```rust,ignore +/// locale_match!(locale => LC_TIME::T_FMT_AMPM) +/// +/// error[E0425]: cannot find value `T_FMT_AMPM` in module `$crate::ff_SN::LC_TIME` +/// --> src/format/locales.rs:72:45 +/// | +/// 72 | ...le => LC_TIME::T_FMT_AMPM); +/// | ^^^^^^^^^^ not found in `$crate::ff_SN::LC_TIME` +/// | +/// help: consider importing one of these items +/// | +/// 1 | use pure_rust_locales::POSIX::LC_TIME::T_FMT_AMPM; +/// | +/// 1 | use pure_rust_locales::aa_DJ::LC_TIME::T_FMT_AMPM; +/// | +/// 1 | use pure_rust_locales::aa_ER::LC_TIME::T_FMT_AMPM; +/// | +/// 1 | use pure_rust_locales::aa_ER_saaho::LC_TIME::T_FMT_AMPM; +/// | +/// and 285 other candidates +/// ``` +/// +/// [Issue #4]: https://github.com/cecton/pure-rust-locales/issues/4 +pub(crate) fn t_fmt_ampm(locale: Locale) -> &'static str { + #[allow(unused_imports)] + use pure_rust_locales::Locale::*; + + locale_match_ampm!(locale => ( + POSIX, + aa_DJ, aa_ER, aa_ER_saaho, aa_ET, af_ZA, agr_PE, ak_GH, am_ET, an_ES, anp_IN, ar_AE, ar_BH, ar_DZ, ar_EG, ar_IN, ar_IQ, ar_JO, ar_KW, ar_LB, ar_LY, ar_MA, ar_OM, ar_QA, ar_SA, ar_SD, ar_SS, ar_SY, ar_TN, ar_YE, as_IN, ast_ES, ayc_PE, az_AZ, az_IR, + be_BY, be_BY_latin, bem_ZM, ber_DZ, ber_MA, bg_BG, bhb_IN, bho_IN, bho_NP, bi_VU, bn_BD, bn_IN, bo_CN, bo_IN, br_FR, br_FR_euro, brx_IN, bs_BA, byn_ER, + ca_AD, ca_ES, ca_ES_euro, ca_ES_valencia, ca_FR, ca_IT, ce_RU, chr_US, cmn_TW, crh_UA, cs_CZ, csb_PL, cv_RU, cy_GB, + da_DK, de_AT, de_AT_euro, de_BE, de_BE_euro, de_CH, de_DE, de_DE_euro, de_IT, de_LI, de_LU, de_LU_euro, doi_IN, dsb_DE, dv_MV, dz_BT, + el_CY, el_GR, el_GR_euro, en_AG, en_AU, en_BW, en_CA, en_DK, en_GB, en_HK, en_IE, en_IE_euro, en_IL, en_IN, en_NG, en_NZ, en_PH, en_SC, en_SG, en_US, en_ZA, en_ZM, en_ZW, eo, es_AR, es_BO, es_CL, es_CO, es_CR, es_CU, es_DO, es_EC, es_ES, es_ES_euro, es_GT, es_HN, es_MX, es_NI, es_PA, es_PE, es_PR, es_PY, es_SV, es_US, es_UY, es_VE, et_EE, eu_ES, eu_ES_euro, + fa_IR, /* ff_SN, */ fi_FI, fi_FI_euro, fil_PH, fo_FO, fr_BE, fr_BE_euro, fr_CA, fr_CH, fr_FR, fr_FR_euro, fr_LU, fr_LU_euro, fur_IT, fy_DE, fy_NL, + ga_IE, ga_IE_euro, gd_GB, gez_ER, gez_ER_abegede, gez_ET, gez_ET_abegede, gl_ES, gl_ES_euro, gu_IN, gv_GB, + ha_NG, hak_TW, he_IL, hi_IN, hif_FJ, hne_IN, hr_HR, hsb_DE, ht_HT, hu_HU, hy_AM, + ia_FR, id_ID, ig_NG, ik_CA, is_IS, it_CH, it_IT, it_IT_euro, iu_CA, + ja_JP, + ka_GE, kab_DZ, kk_KZ, kl_GL, /* km_KH, */ kn_IN, ko_KR, kok_IN, ks_IN, ks_IN_devanagari, ku_TR, kw_GB, ky_KG, + lb_LU, lg_UG, li_BE, li_NL, lij_IT, ln_CD, lo_LA, lt_LT, lv_LV, lzh_TW, + mag_IN, mai_IN, mai_NP, mfe_MU, mg_MG, mhr_RU, mi_NZ, miq_NI, mjw_IN, mk_MK, ml_IN, mn_MN, mni_IN, mnw_MM, mr_IN, ms_MY, mt_MT, my_MM, + nan_TW, nan_TW_latin, nb_NO, nds_DE, nds_NL, ne_NP, nhn_MX, niu_NU, niu_NZ, nl_AW, nl_BE, nl_BE_euro, nl_NL, nl_NL_euro, nn_NO, nr_ZA, nso_ZA, + oc_FR, om_ET, om_KE, or_IN, os_RU, + pa_IN, pa_PK, pap_AW, pap_CW, pl_PL, ps_AF, pt_BR, pt_PT, pt_PT_euro, + quz_PE, + raj_IN, ro_RO, ru_RU, ru_UA, rw_RW, + sa_IN, sah_RU, sat_IN, sc_IT, sd_IN, sd_IN_devanagari, se_NO, sgs_LT, shn_MM, shs_CA, si_LK, sid_ET, sk_SK, sl_SI, sm_WS, so_DJ, so_ET, so_KE, so_SO, sq_AL, sq_MK, sr_ME, sr_RS, sr_RS_latin, ss_ZA, st_ZA, sv_FI, sv_FI_euro, sv_SE, sw_KE, sw_TZ, szl_PL, + ta_IN, ta_LK, tcy_IN, te_IN, tg_TJ, th_TH, the_NP, ti_ER, ti_ET, tig_ER, tk_TM, tl_PH, tn_ZA, to_TO, tpi_PG, tr_CY, tr_TR, ts_ZA, tt_RU, tt_RU_iqtelif, + /* ug_CN, */ uk_UA, unm_US, ur_IN, ur_PK, uz_UZ, uz_UZ_cyrillic, + ve_ZA, vi_VN, + wa_BE, wa_BE_euro, wae_CH, wal_ET, wo_SN, + xh_ZA, + yi_US, yo_NG, yue_HK, yuw_PG, + zh_CN, zh_HK, zh_SG, zh_TW, zu_ZA, + )) +} diff --git a/src/format/strftime.rs b/src/format/strftime.rs index 24bae20c0a..b90beb360c 100644 --- a/src/format/strftime.rs +++ b/src/format/strftime.rs @@ -65,7 +65,7 @@ The following specifiers are available both to formatting and parsing. | `%R` | `00:34` | Hour-minute format. Same as `%H:%M`. | | `%T` | `00:34:60` | Hour-minute-second format. Same as `%H:%M:%S`. | | `%X` | `00:34:60` | Locale's time representation (e.g., 23:13:48). | -| `%r` | `12:34:60 AM` | Hour-minute-second format in 12-hour clocks. Same as `%I:%M:%S %p`. | +| `%r` | `12:34:60 AM` | Locale's 12 hour clock time. (e.g., 11:11:04 PM) | | | | | | | | **TIME ZONE SPECIFIERS:** | | `%Z` | `ACST` | Local time zone name. Skips all non-whitespace characters during parsing. Identical to `%:z` when formatting. [^8] | @@ -213,6 +213,8 @@ static D_T_FMT: &[Item<'static>] = &[ num0!(Year), ]; static T_FMT: &[Item<'static>] = &[num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)]; +static T_FMT_AMPM: &[Item<'static>] = + &[num0!(Hour12), lit!(":"), num0!(Minute), lit!(":"), num0!(Second), sp!(" "), fix!(UpperAmPm)]; /// Parsing iterator for `strftime`-like format strings. #[derive(Clone, Debug)] @@ -229,6 +231,8 @@ pub struct StrftimeItems<'a> { d_t_fmt: Fmt<'a>, /// Time format t_fmt: Fmt<'a>, + /// 12 hour Time format with AM/PM + t_fmt_ampm: Fmt<'a>, } impl<'a> StrftimeItems<'a> { @@ -243,11 +247,20 @@ impl<'a> StrftimeItems<'a> { #[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))] #[must_use] pub fn new_with_locale(s: &'a str, locale: Locale) -> StrftimeItems<'a> { - let d_fmt = StrftimeItems::new(locales::d_fmt(locale)).collect(); - let d_t_fmt = StrftimeItems::new(locales::d_t_fmt(locale)).collect(); - let t_fmt = StrftimeItems::new(locales::t_fmt(locale)).collect(); + let d_fmt: Vec = StrftimeItems::new(locales::d_fmt(locale)).collect(); + let t_fmt: Vec = StrftimeItems::new(locales::t_fmt(locale)).collect(); + let t_fmt_ampm: Vec = StrftimeItems::new(locales::t_fmt_ampm(locale)).collect(); + let d_t_fmt = StrftimeItems { + remainder: locales::d_t_fmt(locale), + recons: Vec::new(), + d_fmt: d_fmt.clone(), + t_fmt: t_fmt.clone(), + t_fmt_ampm: t_fmt_ampm.clone(), + d_t_fmt: D_T_FMT.to_vec(), + } + .collect(); - StrftimeItems { remainder: s, recons: Vec::new(), d_fmt, d_t_fmt, t_fmt } + StrftimeItems { remainder: s, recons: Vec::new(), d_fmt, d_t_fmt, t_fmt, t_fmt_ampm } } #[cfg(not(feature = "unstable-locales"))] @@ -259,6 +272,7 @@ impl<'a> StrftimeItems<'a> { recons: FMT_NONE, d_fmt: D_FMT, d_t_fmt: D_T_FMT, + t_fmt_ampm: T_FMT_AMPM, t_fmt: T_FMT, } } @@ -270,6 +284,7 @@ impl<'a> StrftimeItems<'a> { recons: Vec::new(), d_fmt: D_FMT.to_vec(), d_t_fmt: D_T_FMT.to_vec(), + t_fmt_ampm: T_FMT_AMPM.to_vec(), t_fmt: T_FMT.to_vec(), } } @@ -395,15 +410,7 @@ impl<'a> Iterator for StrftimeItems<'a> { 'm' => num0!(Month), 'n' => sp!("\n"), 'p' => fix!(UpperAmPm), - 'r' => recons![ - num0!(Hour12), - lit!(":"), - num0!(Minute), - lit!(":"), - num0!(Second), - sp!(" "), - fix!(UpperAmPm) - ], + 'r' => recons_from_slice!(self.t_fmt_ampm), 's' => num!(Timestamp), 't' => sp!("\t"), 'u' => num!(WeekdayFromMon), @@ -668,18 +675,26 @@ fn test_strftime_docs() { assert_eq!(dt.format("%%").to_string(), "%"); } +/// helper function to setup a date time for the localized tests +#[cfg(feature = "unstable-locales")] +#[cfg(test)] +fn datetime_for_localized_tests() -> crate::DateTime { + use crate::{FixedOffset, NaiveDate}; + + NaiveDate::from_ymd_opt(2001, 7, 8) + .unwrap() + .and_hms_nano_opt(0, 34, 59, 1_026_490_708) + .unwrap() + .and_local_timezone(FixedOffset::east_opt(34200).unwrap()) + .unwrap() +} + #[cfg(feature = "unstable-locales")] #[test] fn test_strftime_docs_localized() { - use crate::{FixedOffset, NaiveDate, TimeZone}; - - let dt = FixedOffset::east_opt(34200).unwrap().ymd_opt(2001, 7, 8).unwrap().and_hms_nano( - 0, - 34, - 59, - 1_026_490_708, - ); + use crate::NaiveDate; + let dt = datetime_for_localized_tests(); // date specifiers assert_eq!(dt.format_localized("%b", Locale::fr_BE).to_string(), "jui"); assert_eq!(dt.format_localized("%B", Locale::fr_BE).to_string(), "juillet"); @@ -718,3 +733,48 @@ fn test_strftime_docs_localized() { assert_eq!(nd.format_localized("%F", Locale::de_DE).to_string(), "2001-07-08"); assert_eq!(nd.format_localized("%v", Locale::de_DE).to_string(), " 8-Jul-2001"); } + +#[cfg(feature = "unstable-locales")] +#[test] +fn test_strftime_localized_korean() { + let dt = datetime_for_localized_tests(); + + // date specifiers + assert_eq!(dt.format_localized("%b", Locale::ko_KR).to_string(), " 7월"); + assert_eq!(dt.format_localized("%B", Locale::ko_KR).to_string(), "7월"); + assert_eq!(dt.format_localized("%h", Locale::ko_KR).to_string(), " 7월"); + assert_eq!(dt.format_localized("%a", Locale::ko_KR).to_string(), "일"); + assert_eq!(dt.format_localized("%A", Locale::ko_KR).to_string(), "일요일"); + assert_eq!(dt.format_localized("%D", Locale::ko_KR).to_string(), "07/08/01"); + assert_eq!(dt.format_localized("%x", Locale::ko_KR).to_string(), "2001년 07월 08일"); + assert_eq!(dt.format_localized("%F", Locale::ko_KR).to_string(), "2001-07-08"); + assert_eq!(dt.format_localized("%v", Locale::ko_KR).to_string(), " 8- 7월-2001"); + assert_eq!(dt.format_localized("%r", Locale::ko_KR).to_string(), "오전 12시 34분 60초"); + + // date & time specifiers + assert_eq!( + dt.format_localized("%c", Locale::ko_KR).to_string(), + "2001년 07월 08일 (일) 오전 12시 34분 60초" + ); +} + +#[cfg(feature = "unstable-locales")] +#[test] +fn test_strftime_localized_japanese() { + let dt = datetime_for_localized_tests(); + + // date specifiers + assert_eq!(dt.format_localized("%b", Locale::ja_JP).to_string(), " 7月"); + assert_eq!(dt.format_localized("%B", Locale::ja_JP).to_string(), "7月"); + assert_eq!(dt.format_localized("%h", Locale::ja_JP).to_string(), " 7月"); + assert_eq!(dt.format_localized("%a", Locale::ja_JP).to_string(), "日"); + assert_eq!(dt.format_localized("%A", Locale::ja_JP).to_string(), "日曜日"); + assert_eq!(dt.format_localized("%D", Locale::ja_JP).to_string(), "07/08/01"); + assert_eq!(dt.format_localized("%x", Locale::ja_JP).to_string(), "2001年07月08日"); + assert_eq!(dt.format_localized("%F", Locale::ja_JP).to_string(), "2001-07-08"); + assert_eq!(dt.format_localized("%v", Locale::ja_JP).to_string(), " 8- 7月-2001"); + assert_eq!(dt.format_localized("%r", Locale::ja_JP).to_string(), "午前12時34分60秒"); + + // date & time specifiers + assert_eq!(dt.format_localized("%c", Locale::ja_JP).to_string(), "2001年07月08日 00時34分60秒"); +}