Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix locale formatting for %c and %r #1058

Closed
wants to merge 10 commits into from
112 changes: 112 additions & 0 deletions src/format/locales.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
///
/// it 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.
/// fallbacks to en_US if the field is an empty string.
///
scarf005 marked this conversation as resolved.
Show resolved Hide resolved
/// ```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 locale's time format in 12 hour AM/PM.
scarf005 marked this conversation as resolved.
Show resolved Hide resolved
///
/// uses hardcoded [locale_match_ampm] macro to find `LC_TIME::T_FMT_AMPM` field.
///
/// ## Why locale_match won't work
///
/// [locale_match] assumes every single locale module to have same field.
scarf005 marked this conversation as resolved.
Show resolved Hide resolved
/// however, due to issues in [pure-rust-locales][issue],
scarf005 marked this conversation as resolved.
Show resolved Hide resolved
/// three locales (ff_SN, km_KH, ug_CN) are missing `LC_TIME::T_FMT_AMPM` field,
/// causing compile error.
scarf005 marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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]: 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,
))
}
104 changes: 82 additions & 22 deletions src/format/strftime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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] |
Expand Down Expand Up @@ -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)]
Expand All @@ -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> {
Expand All @@ -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<Item> = StrftimeItems::new(locales::d_fmt(locale)).collect();
let t_fmt: Vec<Item> = StrftimeItems::new(locales::t_fmt(locale)).collect();
let t_fmt_ampm: Vec<Item> = 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"))]
Expand All @@ -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,
}
}
Expand All @@ -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(),
}
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 setup_naive_dt() -> crate::DateTime<crate::FixedOffset> {
scarf005 marked this conversation as resolved.
Show resolved Hide resolved
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 = setup_naive_dt();
// 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");
Expand Down Expand Up @@ -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 = setup_naive_dt();

// 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 = setup_naive_dt();

// 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秒");
}