use crate::reference::{EdtfString, Reference};
use crate::values::{ComponentValues, ProcHints, ProcValues, RenderOptions};
use citum_edtf::{Day, Edtf, MonthOrSeason, Timezone, UnspecifiedYear, Year};
use citum_schema::locale::{GeneralTerm, TermForm};
use citum_schema::options::dates::TimeFormat;
use citum_schema::reference::types::RefDate;
use citum_schema::template::{DateForm, DateVariable as TemplateDateVar, TemplateDate};
fn month_to_string(month: u32, months: &[String]) -> String {
if month > 0 {
let index = month - 1;
if let Some(month_name) = months.get(index as usize) {
month_name.clone()
} else {
String::new()
}
} else {
String::new()
}
}
fn extract_month(date: &EdtfString, months: &[String]) -> String {
let parsed_date = date.parse();
let month: Option<u32> = match parsed_date {
RefDate::Edtf(edtf) => edtf.month(),
RefDate::Literal(_) => None,
};
match month {
Some(month) => month_to_string(month, months),
None => String::new(),
}
}
fn unspecified_year_delta(u: &UnspecifiedYear) -> i64 {
match u {
UnspecifiedYear::None => 0,
UnspecifiedYear::One => 9,
UnspecifiedYear::Two => 99,
UnspecifiedYear::Three => 999,
UnspecifiedYear::Four => 9999,
}
}
fn format_display_year(
year: &Year,
date_terms: &citum_schema::locale::DateTerms,
era_labels: &citum_schema::options::dates::EraLabels,
_neg_unspecified: &citum_schema::options::dates::NegativeUnspecifiedYears,
range_delimiter: &str,
) -> String {
if year.unspecified != UnspecifiedYear::None && year.value > 0 {
let mut s = year.value.to_string();
let unspec_count = match year.unspecified {
UnspecifiedYear::One => 1,
UnspecifiedYear::Two => 2,
UnspecifiedYear::Three => 3,
UnspecifiedYear::Four => 4,
_ => 0,
};
for _ in 0..unspec_count {
if let Some(last) = s.pop()
&& last != '0'
{
s.push('X');
}
}
if s.len() < year.value.to_string().len() {
let diff = year.value.to_string().len() - s.len();
for _ in 0..diff {
s.push('X');
}
}
return s;
}
if year.unspecified != UnspecifiedYear::None && year.value <= 0 {
let delta = unspecified_year_delta(&year.unspecified);
let astronomical_min = year.value - delta;
let astronomical_max = year.value;
let historical_end = 1 - astronomical_max;
let historical_start = 1 - astronomical_min;
let era_term = match era_labels {
citum_schema::options::dates::EraLabels::Default => {
date_terms.before_era.as_deref().unwrap_or("")
}
citum_schema::options::dates::EraLabels::BcAd => date_terms.bc.as_deref().unwrap_or(""),
citum_schema::options::dates::EraLabels::BceCe => {
date_terms.bce.as_deref().unwrap_or("")
}
};
if era_term.is_empty() {
format!("{historical_start}{range_delimiter}{historical_end}")
} else {
format!("{historical_start}{range_delimiter}{historical_end} {era_term}")
}
} else if year.value <= 0 {
let historical_year = 1 - year.value;
let era_term = match era_labels {
citum_schema::options::dates::EraLabels::Default => {
date_terms.before_era.as_deref().unwrap_or("")
}
citum_schema::options::dates::EraLabels::BcAd => date_terms.bc.as_deref().unwrap_or(""),
citum_schema::options::dates::EraLabels::BceCe => {
date_terms.bce.as_deref().unwrap_or("")
}
};
if era_term.is_empty() {
historical_year.to_string()
} else {
format!("{historical_year} {era_term}")
}
} else {
let era_term = match era_labels {
citum_schema::options::dates::EraLabels::Default => "",
citum_schema::options::dates::EraLabels::BcAd => date_terms.ad.as_deref().unwrap_or(""),
citum_schema::options::dates::EraLabels::BceCe => {
date_terms.ce.as_deref().unwrap_or("")
}
};
if era_term.is_empty() {
year.value.to_string()
} else {
format!("{} {}", year.value, era_term)
}
}
}
fn format_display_year_legacy(year: &Year, before_era: Option<&str>) -> String {
if year.unspecified != UnspecifiedYear::None {
return year.to_string();
}
if year.value <= 0 {
let historical_year = 1 - year.value;
if let Some(term) = before_era.filter(|term| !term.is_empty()) {
format!("{historical_year} {term}")
} else {
historical_year.to_string()
}
} else {
year.value.to_string()
}
}
#[allow(dead_code, reason = "kept for backwards compatibility")]
fn extract_display_year_legacy(date: &EdtfString, before_era: Option<&str>) -> String {
match date.parse() {
RefDate::Edtf(edtf) => match edtf {
Edtf::Date(date) => format_display_year_legacy(&date.year, before_era),
Edtf::Interval(interval) => {
format_display_year_legacy(&interval.start.year, before_era)
}
Edtf::IntervalFrom(date) | Edtf::IntervalTo(date) => {
format_display_year_legacy(&date.year, before_era)
}
},
RefDate::Literal(_) => String::new(),
}
}
fn extract_range_end(
date: &EdtfString,
months: &[String],
date_terms: &citum_schema::locale::DateTerms,
era_labels: &citum_schema::options::dates::EraLabels,
neg_unspecified: &citum_schema::options::dates::NegativeUnspecifiedYears,
range_delimiter: &str,
) -> Option<String> {
match date.parse() {
RefDate::Edtf(edtf) => match edtf {
Edtf::Interval(interval) => {
let end = &interval.end;
let year = format_display_year(
&end.year,
date_terms,
era_labels,
neg_unspecified,
range_delimiter,
);
let month = match end.month_or_season {
Some(MonthOrSeason::Month(m)) => Some(m),
_ => None,
};
let day = match end.day {
Some(Day::Day(d)) => Some(d),
_ => None,
};
match (month, day) {
(Some(m), Some(d)) if m > 0 && d > 0 => {
let month_str = month_to_string(m, months);
Some(format!("{} {}, {}", month_str, d, year))
}
(Some(m), _) if m > 0 => {
let month_str = month_to_string(m, months);
Some(format!("{} {}", month_str, year))
}
_ => Some(year),
}
}
Edtf::IntervalFrom(_date) => None, Edtf::IntervalTo(date) => {
let year = format_display_year(
&date.year,
date_terms,
era_labels,
neg_unspecified,
range_delimiter,
);
Some(year)
}
_ => None,
},
RefDate::Literal(_) => None,
}
}
fn format_time(
time: citum_edtf::Time,
format: &TimeFormat,
show_seconds: bool,
show_timezone: bool,
am_term: Option<&str>,
pm_term: Option<&str>,
utc_term: Option<&str>,
) -> String {
let (display_hour, period) = match format {
TimeFormat::Hour12 => {
let (h, p) = if time.hour == 0 {
(12u32, am_term.unwrap_or("AM"))
} else if time.hour < 12 {
(time.hour, am_term.unwrap_or("AM"))
} else if time.hour == 12 {
(12u32, pm_term.unwrap_or("PM"))
} else {
(time.hour - 12, pm_term.unwrap_or("PM"))
};
(h, Some(p))
}
TimeFormat::Hour24 => (time.hour, None),
};
let time_str = if show_seconds {
format!("{:02}:{:02}:{:02}", display_hour, time.minute, time.second)
} else {
format!("{:02}:{:02}", display_hour, time.minute)
};
let with_period = match period {
Some(p) => format!("{time_str} {p}"),
None => time_str,
};
if show_timezone {
let tz_str = match time.timezone {
Some(Timezone::Utc) => utc_term.unwrap_or("UTC").to_string(),
Some(Timezone::Offset(mins)) => {
let sign = if mins >= 0 { '+' } else { '-' };
let abs = mins.unsigned_abs();
format!("{}{:02}:{:02}", sign, abs / 60, abs % 60)
}
None => String::new(),
};
if tz_str.is_empty() {
with_period
} else {
format!("{with_period} {tz_str}")
}
} else {
with_period
}
}
fn format_range_start(
date: &EdtfString,
form: &DateForm,
locale: &citum_schema::locale::Locale,
date_config: Option<&citum_schema::options::dates::DateConfig>,
) -> String {
let default_era = citum_schema::options::dates::EraLabels::Default;
let default_neg_unspec = citum_schema::options::dates::NegativeUnspecifiedYears::default();
let era_labels = date_config.map(|c| &c.era_labels).unwrap_or(&default_era);
let neg_unspecified = date_config
.map(|c| &c.negative_unspecified_years)
.unwrap_or(&default_neg_unspec);
let range_delimiter = date_config.map_or("–", |c| c.range_delimiter.as_str());
let extract_year = |d: &EdtfString| -> String {
match d.parse() {
RefDate::Edtf(edtf) => match edtf {
Edtf::Date(date) => format_display_year(
&date.year,
&locale.dates,
era_labels,
neg_unspecified,
range_delimiter,
),
Edtf::Interval(interval) => format_display_year(
&interval.start.year,
&locale.dates,
era_labels,
neg_unspecified,
range_delimiter,
),
Edtf::IntervalFrom(date) | Edtf::IntervalTo(date) => format_display_year(
&date.year,
&locale.dates,
era_labels,
neg_unspecified,
range_delimiter,
),
},
RefDate::Literal(_) => String::new(),
}
};
match form {
DateForm::Year => extract_year(date),
DateForm::YearMonth => {
let month = extract_month(date, &locale.dates.months.long);
let year = extract_year(date);
if month.is_empty() {
year
} else {
format!("{month} {year}")
}
}
DateForm::MonthDay => {
let month = extract_month(date, &locale.dates.months.long);
let day = date.day();
match day {
Some(d) => format!("{month} {d}"),
None => month,
}
}
DateForm::Full => {
let year = extract_year(date);
let month = extract_month(date, &locale.dates.months.long);
let day = date.day();
match (month.is_empty(), day) {
(true, _) => year,
(false, None) => format!("{month} {year}"),
(false, Some(d)) => format!("{month} {d}, {year}"),
}
}
DateForm::YearMonthDay => {
let year = extract_year(date);
let month = extract_month(date, &locale.dates.months.long);
let day = date.day();
match (month.is_empty(), day) {
(true, _) => year,
(false, None) => format!("{year}, {month}"),
(false, Some(d)) => format!("{year}, {month} {d}"),
}
}
DateForm::DayMonthAbbrYear => {
let year = extract_year(date);
let month = extract_month(date, &locale.dates.months.short);
let day = date.day();
match (month.is_empty(), day) {
(true, _) => year,
(false, None) => format!("{month} {year}"),
(false, Some(d)) => format!("{d} {month} {year}"),
}
}
DateForm::MonthAbbrDayYear => {
let year = extract_year(date);
let month = extract_month(date, &locale.dates.months.short);
let day = date.day();
match (month.is_empty(), day) {
(true, _) => year,
(false, None) => format!("{month} {year}"),
(false, Some(d)) => format!("{month} {d}, {year}"),
}
}
_ => extract_year(date),
}
}
fn format_date_range(
start: String,
date: &EdtfString,
locale: &citum_schema::locale::Locale,
date_config: Option<&citum_schema::options::dates::DateConfig>,
) -> Option<String> {
let era_labels = date_config
.map(|c| &c.era_labels)
.unwrap_or(&citum_schema::options::dates::EraLabels::Default);
let neg_unspecified = date_config
.map(|c| &c.negative_unspecified_years)
.unwrap_or(&citum_schema::options::dates::NegativeUnspecifiedYears::Range);
let delimiter = date_config.map_or("–", |c| c.range_delimiter.as_str());
if date.is_open_range() {
if let Some(end_marker) = date_config
.and_then(|c| c.open_range_marker.as_deref())
.or(locale.dates.open_ended_term.as_deref())
{
Some(format!("{start}{delimiter}{end_marker}"))
} else {
Some(start)
}
} else if let Some(end) = extract_range_end(
date,
&locale.dates.months.long,
&locale.dates,
era_labels,
neg_unspecified,
delimiter,
) {
Some(format!("{start}{delimiter}{end}"))
} else {
Some(start)
}
}
fn apply_date_markers(
value: String,
date: &EdtfString,
date_config: Option<&citum_schema::options::dates::DateConfig>,
) -> String {
let mut result = value;
if date.is_approximate()
&& let Some(marker) = date_config.and_then(|c| c.approximation_marker.as_ref())
{
result = format!("{marker}{result}");
}
if date.is_uncertain()
&& let Some(marker) = date_config.and_then(|c| c.uncertainty_marker.as_ref())
{
result = format!("{result}{marker}");
}
result
}
fn compute_disamb_suffix<F: crate::render::format::OutputFormat<Output = String>>(
date: &EdtfString,
form: &DateForm,
hints: &ProcHints,
options: &RenderOptions<'_>,
fmt: &F,
) -> Option<String> {
if hints.disamb_condition && date_form_displays_year(form) && !date.year().is_empty() {
let use_suffix = options
.config
.processing
.as_ref()
.unwrap_or(&citum_schema::options::Processing::AuthorDate)
.config()
.disambiguate
.as_ref()
.is_some_and(|d| d.year_suffix);
if use_suffix {
int_to_letter(hints.group_index as u32).map(|s| fmt.text(&s))
} else {
None
}
} else {
None
}
}
fn date_form_displays_year(form: &DateForm) -> bool {
!matches!(form, DateForm::MonthDay)
}
fn inline_disamb_suffix(formatted: &str, form: &DateForm, year: &str, suffix: &str) -> String {
if year.is_empty() || suffix.is_empty() {
return formatted.to_string();
}
let year_index = match form {
DateForm::Year | DateForm::YearMonthDay => formatted.find(year),
DateForm::YearMonth
| DateForm::Full
| DateForm::DayMonthAbbrYear
| DateForm::MonthAbbrDayYear => formatted.rfind(year),
DateForm::MonthDay => None,
_ => None,
};
let Some(index) = year_index else {
return format!("{formatted}{suffix}");
};
let year_end = index + year.len();
#[allow(clippy::string_slice, reason = "indices derived from find/rfind")]
let result = format!(
"{}{}{}{}",
&formatted[..index],
year,
suffix,
&formatted[year_end..]
);
result
}
#[allow(
clippy::too_many_lines,
reason = "date formatting handles 6 form variants"
)]
fn format_single_date(
date: &EdtfString,
form: &DateForm,
locale: &citum_schema::locale::Locale,
date_config: Option<&citum_schema::options::dates::DateConfig>,
) -> Option<String> {
let default_era = citum_schema::options::dates::EraLabels::Default;
let default_neg_unspec = citum_schema::options::dates::NegativeUnspecifiedYears::default();
let era_labels = date_config.map(|c| &c.era_labels).unwrap_or(&default_era);
let neg_unspecified = date_config
.map(|c| &c.negative_unspecified_years)
.unwrap_or(&default_neg_unspec);
let range_delimiter = date_config.map_or("–", |c| c.range_delimiter.as_str());
let extract_year = |d: &EdtfString| -> String {
match d.parse() {
RefDate::Edtf(edtf) => match edtf {
Edtf::Date(dt) => format_display_year(
&dt.year,
&locale.dates,
era_labels,
neg_unspecified,
range_delimiter,
),
Edtf::Interval(interval) => format_display_year(
&interval.start.year,
&locale.dates,
era_labels,
neg_unspecified,
range_delimiter,
),
Edtf::IntervalFrom(dt) | Edtf::IntervalTo(dt) => format_display_year(
&dt.year,
&locale.dates,
era_labels,
neg_unspecified,
range_delimiter,
),
},
RefDate::Literal(_) => String::new(),
}
};
match form {
DateForm::Year => {
let year = extract_year(date);
if year.is_empty() { None } else { Some(year) }
}
DateForm::YearMonth => {
let year = extract_year(date);
if year.is_empty() {
return None;
}
let month = extract_month(date, &locale.dates.months.long);
let month_opt = (!month.is_empty()).then_some(month.as_str());
if let Some(rendered) =
locale.resolve_date_pattern("pattern.date-year-month", Some(&year), month_opt, None)
{
return Some(rendered);
}
if month.is_empty() {
Some(year)
} else {
Some(format!("{month} {year}"))
}
}
DateForm::MonthDay => {
let month = extract_month(date, &locale.dates.months.long);
if month.is_empty() {
return None;
}
let day = date.day();
if let Some(rendered) =
locale.resolve_date_pattern("pattern.date-month-day", None, Some(&month), day)
{
return Some(rendered);
}
match day {
Some(d) => Some(format!("{month} {d}")),
None => Some(month),
}
}
DateForm::Full => {
let year = extract_year(date);
if year.is_empty() {
return None;
}
let month = extract_month(date, &locale.dates.months.long);
let day = date.day();
let base = locale
.resolve_date_pattern(
"pattern.date-full",
Some(&year),
(!month.is_empty()).then_some(month.as_str()),
day,
)
.unwrap_or_else(|| match (month.is_empty(), day) {
(true, _) => year.clone(),
(false, None) => format!("{month} {year}"),
(false, Some(d)) => format!("{month} {d}, {year}"),
});
if let (Some(time_fmt), Some(time)) = (
date_config.and_then(|c| c.time_format.as_ref()),
date.time(),
) {
let show_secs = date_config.is_some_and(|c| c.show_seconds);
let show_tz = date_config.is_some_and(|c| c.show_timezone);
let time_str = format_time(
time,
time_fmt,
show_secs,
show_tz,
locale.dates.am.as_deref(),
locale.dates.pm.as_deref(),
locale.dates.timezone_utc.as_deref(),
);
Some(format!("{base}, {time_str}"))
} else {
Some(base)
}
}
DateForm::YearMonthDay => {
let year = extract_year(date);
if year.is_empty() {
return None;
}
let month = extract_month(date, &locale.dates.months.long);
let day = date.day();
let month_opt = (!month.is_empty()).then_some(month.as_str());
if let Some(rendered) = locale.resolve_date_pattern(
"pattern.date-year-month-day",
Some(&year),
month_opt,
day,
) {
return Some(rendered);
}
match (month.is_empty(), day) {
(true, _) => Some(year),
(false, None) => Some(format!("{year}, {month}")),
(false, Some(d)) => Some(format!("{year}, {month} {d}")),
}
}
DateForm::DayMonthAbbrYear => {
let year = extract_year(date);
if year.is_empty() {
return None;
}
let month = extract_month(date, &locale.dates.months.short);
let day = date.day();
let month_opt = (!month.is_empty()).then_some(month.as_str());
if let Some(rendered) = locale.resolve_date_pattern(
"pattern.date-day-month-abbr-year",
Some(&year),
month_opt,
day,
) {
return Some(rendered);
}
match (month.is_empty(), day) {
(true, _) => Some(year),
(false, None) => Some(format!("{month} {year}")),
(false, Some(d)) => Some(format!("{d} {month} {year}")),
}
}
DateForm::MonthAbbrDayYear => {
let year = extract_year(date);
if year.is_empty() {
return None;
}
let month = extract_month(date, &locale.dates.months.short);
let day = date.day();
let month_opt = (!month.is_empty()).then_some(month.as_str());
if let Some(rendered) = locale.resolve_date_pattern(
"pattern.date-month-abbr-day-year",
Some(&year),
month_opt,
day,
) {
return Some(rendered);
}
match (month.is_empty(), day) {
(true, _) => Some(year),
(false, None) => Some(format!("{month} {year}")),
(false, Some(d)) => Some(format!("{month} {d}, {year}")),
}
}
_ => Some(extract_year(date)),
}
}
impl ComponentValues for TemplateDate {
fn values<F: crate::render::format::OutputFormat<Output = String>>(
&self,
reference: &Reference,
hints: &ProcHints,
options: &RenderOptions<'_>,
) -> Option<ProcValues<F::Output>> {
let fmt = F::default();
let date_opt: Option<EdtfString> = match self.date {
TemplateDateVar::Issued => reference.csl_issued_date(),
TemplateDateVar::Accessed => reference.accessed(),
TemplateDateVar::OriginalPublished => reference.original_date(),
_ => None,
};
let Some(date) = date_opt.filter(|d| !d.0.is_empty()) else {
if let Some(fallbacks) = &self.fallback {
for component in fallbacks {
if let Some(values) = component.values::<F>(reference, hints, options) {
return Some(values);
}
}
}
if matches!(self.date, TemplateDateVar::Issued)
&& let Some(nd) = options.locale.resolved_general_term(
&GeneralTerm::NoDate,
&TermForm::Short,
None,
)
{
return Some(ProcValues {
value: nd,
prefix: None,
suffix: None,
url: None,
substituted_key: None,
pre_formatted: false,
});
}
return None;
};
let locale = options.locale;
let date_config = options.config.dates.as_ref();
let effective_form = self.form.clone();
let formatted = if date.is_range() {
let start = format_range_start(&date, &effective_form, locale, date_config);
format_date_range(start, &date, locale, date_config)
} else {
format_single_date(&date, &effective_form, locale, date_config)
};
let formatted = formatted.map(|value| apply_date_markers(value, &date, date_config));
let disamb_suffix = compute_disamb_suffix(&date, &effective_form, hints, options, &fmt);
formatted.map(|value| {
let (value, suffix) = if let Some(ref suffix) = disamb_suffix {
(
inline_disamb_suffix(&value, &effective_form, &date.year(), suffix),
None,
)
} else {
(value, None)
};
ProcValues {
value,
prefix: None,
suffix,
url: crate::values::resolve_effective_url(
self.links.as_ref(),
options.config.links.as_ref(),
reference,
citum_schema::options::LinkAnchor::Component,
),
substituted_key: None,
pre_formatted: false,
}
})
}
}
#[must_use]
pub fn int_to_letter(n: u32) -> Option<String> {
if n == 0 {
return None;
}
let mut result = String::new();
let mut num = n - 1;
loop {
result.push((b'a' + (num % 26) as u8) as char);
if num < 26 {
break;
}
num = num / 26 - 1;
}
Some(result.chars().rev().collect())
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
use super::*;
#[test]
fn test_int_to_letter() {
assert_eq!(int_to_letter(1), Some("a".to_string()));
assert_eq!(int_to_letter(2), Some("b".to_string()));
assert_eq!(int_to_letter(26), Some("z".to_string()));
assert_eq!(int_to_letter(27), Some("aa".to_string()));
assert_eq!(int_to_letter(52), Some("az".to_string()));
assert_eq!(int_to_letter(53), Some("ba".to_string()));
assert_eq!(int_to_letter(0), None);
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod time_tests {
use super::*;
use citum_edtf::{Time, Timezone};
#[test]
fn test_format_time_12h_utc() {
let time = Time {
hour: 23,
minute: 20,
second: 30,
timezone: Some(Timezone::Utc),
};
let result = format_time(
time,
&TimeFormat::Hour12,
false,
true,
Some("AM"),
Some("PM"),
Some("UTC"),
);
assert_eq!(result, "11:20 PM UTC");
}
#[test]
fn test_format_time_24h_utc() {
let time = Time {
hour: 23,
minute: 20,
second: 30,
timezone: Some(Timezone::Utc),
};
let result = format_time(
time,
&TimeFormat::Hour24,
false,
true,
None,
None,
Some("UTC"),
);
assert_eq!(result, "23:20 UTC");
}
#[test]
fn test_format_time_with_offset() {
let time = Time {
hour: 10,
minute: 10,
second: 10,
timezone: Some(Timezone::Offset(330)),
};
let result = format_time(
time,
&TimeFormat::Hour24,
false,
true,
None,
None,
Some("UTC"),
);
assert_eq!(result, "10:10 +05:30");
}
#[test]
fn test_format_time_no_timezone() {
let time = Time {
hour: 14,
minute: 30,
second: 0,
timezone: None,
};
let result = format_time(time, &TimeFormat::Hour24, false, false, None, None, None);
assert_eq!(result, "14:30");
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod era_tests {
use super::*;
use citum_edtf::{UnspecifiedYear, Year};
use citum_schema::locale::DateTerms;
use citum_schema::options::dates::{EraLabels, NegativeUnspecifiedYears};
fn en_terms() -> DateTerms {
DateTerms::en_us()
}
#[test]
fn positive_year_default_no_suffix() {
let year = Year {
value: 54,
unspecified: UnspecifiedYear::None,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::Default,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "54");
}
#[test]
fn positive_year_bc_ad() {
let year = Year {
value: 54,
unspecified: UnspecifiedYear::None,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::BcAd,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "54 AD");
}
#[test]
fn positive_year_bce_ce() {
let year = Year {
value: 54,
unspecified: UnspecifiedYear::None,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::BceCe,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "54 CE");
}
#[test]
fn negative_year_default() {
let year = Year {
value: -43,
unspecified: UnspecifiedYear::None,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::Default,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "44 BC");
}
#[test]
fn negative_year_bc_ad() {
let year = Year {
value: -43,
unspecified: UnspecifiedYear::None,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::BcAd,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "44 BC");
}
#[test]
fn negative_year_bce_ce() {
let year = Year {
value: -43,
unspecified: UnspecifiedYear::None,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::BceCe,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "44 BCE");
}
#[test]
fn positive_unspecified_ones() {
let year = Year {
value: 1990,
unspecified: UnspecifiedYear::One,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::Default,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "199X");
}
#[test]
fn positive_unspecified_two() {
let year = Year {
value: 1900,
unspecified: UnspecifiedYear::Two,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::Default,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "19XX");
}
#[test]
fn negative_unspecified_range() {
let year = Year {
value: -90,
unspecified: UnspecifiedYear::One,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::Default,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "100–91 BC");
}
#[test]
fn negative_unspecified_century() {
let year = Year {
value: 0,
unspecified: UnspecifiedYear::Two,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::Default,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "100–1 BC");
}
#[test]
fn backwards_compat_negative_year() {
let year = Year {
value: -99,
unspecified: UnspecifiedYear::None,
};
let result = format_display_year(
&year,
&en_terms(),
&EraLabels::Default,
&NegativeUnspecifiedYears::Range,
"–",
);
assert_eq!(result, "100 BC");
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "Panicking is acceptable in tests."
)]
mod locale_pattern_tests {
use super::*;
use citum_schema::locale::Locale;
fn en_us() -> Locale {
Locale::from_yaml_str(include_str!("../../../../locales/en-US.yaml"))
.expect("en-US locale should parse")
}
fn es_es() -> Locale {
Locale::from_yaml_str(include_str!("../../../../locales/es-ES.yaml"))
.expect("es-ES locale should parse")
}
fn eu_es() -> Locale {
Locale::from_yaml_str(include_str!("../../../../locales/eu-ES.yaml"))
.expect("eu-ES locale should parse")
}
fn full(locale: &Locale, edtf: &str) -> String {
format_single_date(&EdtfString(edtf.to_string()), &DateForm::Full, locale, None)
.expect("date should render")
}
fn month_day(locale: &Locale, edtf: &str) -> String {
format_single_date(
&EdtfString(edtf.to_string()),
&DateForm::MonthDay,
locale,
None,
)
.expect("date should render")
}
#[test]
fn en_us_full_unchanged_by_pattern_machinery() {
assert_eq!(full(&en_us(), "2023-01-12"), "January 12, 2023");
}
#[test]
fn en_us_month_day_unchanged_by_pattern_machinery() {
assert_eq!(month_day(&en_us(), "2023-01-12"), "January 12");
}
#[test]
fn es_es_full_uses_locale_pattern() {
assert_eq!(full(&es_es(), "2023-01-12"), "12 de enero de 2023");
}
#[test]
fn es_es_month_day_uses_locale_pattern() {
assert_eq!(month_day(&es_es(), "2023-01-12"), "12 de enero");
}
#[test]
fn eu_es_full_uses_locale_pattern() {
assert_eq!(full(&eu_es(), "2023-01-12"), "2023ko urtarrilaren 12a");
}
#[test]
fn eu_es_month_day_uses_locale_pattern() {
assert_eq!(month_day(&eu_es(), "2023-01-12"), "urtarrilaren 12a");
}
fn year_month(locale: &Locale, edtf: &str) -> String {
format_single_date(
&EdtfString(edtf.to_string()),
&DateForm::YearMonth,
locale,
None,
)
.expect("date should render")
}
fn year_month_day(locale: &Locale, edtf: &str) -> String {
format_single_date(
&EdtfString(edtf.to_string()),
&DateForm::YearMonthDay,
locale,
None,
)
.expect("date should render")
}
fn day_month_abbr_year(locale: &Locale, edtf: &str) -> String {
format_single_date(
&EdtfString(edtf.to_string()),
&DateForm::DayMonthAbbrYear,
locale,
None,
)
.expect("date should render")
}
fn month_abbr_day_year(locale: &Locale, edtf: &str) -> String {
format_single_date(
&EdtfString(edtf.to_string()),
&DateForm::MonthAbbrDayYear,
locale,
None,
)
.expect("date should render")
}
#[test]
fn en_us_year_month_unchanged_by_pattern_machinery() {
assert_eq!(year_month(&en_us(), "2023-01"), "January 2023");
}
#[test]
fn en_us_year_month_day_unchanged_by_pattern_machinery() {
assert_eq!(year_month_day(&en_us(), "2023-01-12"), "2023, January 12");
}
#[test]
fn en_us_day_month_abbr_year_unchanged_by_pattern_machinery() {
assert_eq!(day_month_abbr_year(&en_us(), "2023-01-12"), "12 Jan. 2023");
}
#[test]
fn en_us_month_abbr_day_year_unchanged_by_pattern_machinery() {
assert_eq!(month_abbr_day_year(&en_us(), "2023-01-12"), "Jan. 12, 2023");
}
#[test]
fn es_es_year_month_uses_locale_pattern() {
assert_eq!(year_month(&es_es(), "2023-01"), "enero de 2023");
}
#[test]
fn eu_es_year_month_uses_locale_pattern() {
assert_eq!(year_month(&eu_es(), "2023-01"), "2023ko urtarrila");
}
#[test]
fn year_month_missing_month_falls_back_to_year() {
assert_eq!(year_month(&es_es(), "2023"), "2023");
}
#[test]
fn es_es_year_month_day_uses_locale_pattern() {
assert_eq!(year_month_day(&es_es(), "2023-01-12"), "2023, 12 de enero");
}
#[test]
fn es_es_year_month_day_missing_day_falls_back() {
assert_eq!(year_month_day(&es_es(), "2023-01"), "2023, enero");
}
#[test]
fn es_es_day_month_abbr_year_uses_locale_pattern() {
assert_eq!(
day_month_abbr_year(&es_es(), "2023-01-12"),
"12 ene. de 2023"
);
}
#[test]
fn es_es_day_month_abbr_year_missing_day_falls_back() {
assert_eq!(day_month_abbr_year(&es_es(), "2023-01"), "ene. 2023");
}
#[test]
fn es_es_month_abbr_day_year_uses_locale_pattern() {
assert_eq!(
month_abbr_day_year(&es_es(), "2023-01-12"),
"ene. 12 de 2023"
);
}
#[test]
fn es_es_month_abbr_day_year_missing_day_falls_back() {
assert_eq!(month_abbr_day_year(&es_es(), "2023-01"), "ene. 2023");
}
#[test]
fn pattern_missing_day_falls_back_to_english_assembly() {
assert_eq!(full(&es_es(), "2023-01"), "enero 2023");
}
}