use crate::cldr::{calendar_spec, CalendarSpec};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DateTime {
pub year: i32,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub millisecond: u16,
}
impl Default for DateTime {
fn default() -> Self {
DateTime {
year: 1970,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
}
}
}
impl DateTime {
#[must_use]
pub fn to_iso8601(&self) -> String {
let base = alloc::format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second
);
if self.millisecond == 0 {
base
} else {
alloc::format!("{base}.{:03}", self.millisecond)
}
}
#[must_use]
pub fn weekday(&self) -> u8 {
crate::calendar::day_of_week(self.year as i64, self.month as i64, self.day as i64)
}
#[must_use]
pub fn add_seconds(&self, delta: i64) -> DateTime {
let jdn =
crate::calendar::gregorian_to_jdn(self.year as i64, self.month as i64, self.day as i64);
let total = jdn
.saturating_mul(86_400)
.saturating_add(self.hour as i64 * 3600)
.saturating_add(self.minute as i64 * 60)
.saturating_add(self.second as i64)
.saturating_add(delta);
let (new_jdn, sod) = (total.div_euclid(86_400), total.rem_euclid(86_400));
let (y, m, d) = crate::calendar::jdn_to_gregorian(new_jdn);
DateTime {
year: y as i32,
month: m as u8,
day: d as u8,
hour: (sod / 3600) as u8,
minute: (sod % 3600 / 60) as u8,
second: (sod % 60) as u8,
millisecond: self.millisecond,
}
}
#[must_use]
pub fn add_days(&self, delta: i64) -> DateTime {
self.add_seconds(delta * 86_400)
}
#[must_use]
pub fn parse_iso8601(s: &str) -> Option<DateTime> {
let s = s.trim().trim_end_matches('Z');
let (date, time) = match s.split_once(['T', ' ']) {
Some((d, t)) => (d, Some(t)),
None => (s, None),
};
let mut dp = date.split('-');
let year: i32 = dp.next()?.parse().ok()?;
let month: u8 = dp.next()?.parse().ok()?;
let day: u8 = dp.next()?.parse().ok()?;
if dp.next().is_some() || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let (hour, minute, second, millisecond) = match time {
None => (0, 0, 0, 0),
Some(t) => {
let t = t.split(['+', '-']).next().unwrap_or(t);
let mut tp = t.split(':');
let h: u8 = tp.next()?.parse().ok()?;
let mi: u8 = tp.next()?.parse().ok()?;
let (se, ms) = match tp.next() {
Some(x) => {
let (whole, frac) = match x.split_once(['.', ',']) {
Some((w, f)) => (w, Some(f)),
None => (x, None),
};
let se: u8 = whole.parse().ok()?;
let ms = match frac {
None | Some("") => 0u16,
Some(f) => {
if !f.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let take = &f[..f.len().min(3)];
let v: u16 = take.parse().ok()?;
v * 10u16.pow(3 - take.len() as u32)
}
};
(se, ms)
}
None => (0, 0),
};
(h, mi, se, ms)
}
};
Some(DateTime {
year,
month,
day,
hour,
minute,
second,
millisecond,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DateTimePartType {
Weekday,
Era,
Year,
Month,
Day,
Hour,
Minute,
Second,
FractionalSecond,
DayPeriod,
TimeZoneName,
Literal,
}
impl DateTimePartType {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
DateTimePartType::Weekday => "weekday",
DateTimePartType::Era => "era",
DateTimePartType::Year => "year",
DateTimePartType::Month => "month",
DateTimePartType::Day => "day",
DateTimePartType::Hour => "hour",
DateTimePartType::Minute => "minute",
DateTimePartType::Second => "second",
DateTimePartType::FractionalSecond => "fractionalSecond",
DateTimePartType::DayPeriod => "dayPeriod",
DateTimePartType::TimeZoneName => "timeZoneName",
DateTimePartType::Literal => "literal",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DateTimePart {
pub kind: DateTimePartType,
pub value: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DateStyle {
Full,
Long,
Medium,
Short,
}
impl DateStyle {
fn idx(self) -> usize {
match self {
DateStyle::Full => 0,
DateStyle::Long => 1,
DateStyle::Medium => 2,
DateStyle::Short => 3,
}
}
}
fn weekday(dt: &DateTime) -> usize {
let t = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
let m = (dt.month as i64).clamp(1, 12);
let d = dt.day as i64;
let y = if m < 3 {
dt.year as i64 - 1
} else {
dt.year as i64
};
(y + y / 4 - y / 100 + y / 400 + t[(m - 1) as usize] + d).rem_euclid(7) as usize
}
fn two(n: i64) -> String {
alloc::format!("{n:02}")
}
fn field(field: char, n: usize, dt: &DateTime, s: &CalendarSpec) -> String {
let m = dt.month as usize;
let mi = m.clamp(1, 12) - 1;
match field {
'y' | 'Y' => {
if n == 2 {
two((dt.year.rem_euclid(100)) as i64)
} else {
dt.year.to_string()
}
}
'M' | 'L' => match n {
1 => m.to_string(),
2 => two(m as i64),
3 => s.months_abbr[mi].to_string(),
5 => s.months_narrow[mi].to_string(),
_ => s.months_wide[mi].to_string(),
},
'd' => {
if n >= 2 {
two(dt.day as i64)
} else {
dt.day.to_string()
}
}
'E' | 'e' | 'c' => {
let w = weekday(dt);
if n == 5 {
s.days_narrow[w].to_string()
} else if n >= 4 {
s.days_wide[w].to_string()
} else {
s.days_abbr[w].to_string()
}
}
'G' => {
let idx = usize::from(dt.year > 0);
if n >= 5 {
s.eras_narrow[idx]
} else if n == 4 {
s.eras_wide[idx]
} else {
s.eras_abbr[idx]
}
.to_string()
}
'h' => {
let h = ((dt.hour as u16 + 11) % 12) + 1; if n >= 2 {
two(h as i64)
} else {
h.to_string()
}
}
'H' => {
if n >= 2 {
two(dt.hour as i64)
} else {
dt.hour.to_string()
}
}
'm' => {
if n >= 2 {
two(dt.minute as i64)
} else {
dt.minute.to_string()
}
}
's' => {
if n >= 2 {
two(dt.second as i64)
} else {
dt.second.to_string()
}
}
'a' | 'b' | 'B' => if dt.hour < 12 { s.am } else { s.pm }.to_string(),
'S' => {
let ms = dt.millisecond.min(999);
let base = alloc::format!("{ms:03}");
if n <= 3 {
base[..n].to_string()
} else {
let mut out = base;
for _ in 0..(n - 3) {
out.push('0');
}
out
}
}
_ => String::new(), }
}
fn part_type(ch: char) -> DateTimePartType {
use DateTimePartType::*;
match ch {
'y' | 'Y' | 'u' | 'U' | 'r' => Year,
'M' | 'L' => Month,
'd' | 'D' | 'F' | 'g' => Day,
'E' | 'e' | 'c' => Weekday,
'h' | 'H' | 'k' | 'K' => Hour,
'm' => Minute,
's' => Second,
'S' | 'A' => FractionalSecond,
'a' | 'b' | 'B' => DayPeriod,
'G' => Era,
'z' | 'Z' | 'O' | 'v' | 'V' | 'X' | 'x' => TimeZoneName,
_ => Literal,
}
}
fn render_parts(pattern: &str, dt: &DateTime, s: &CalendarSpec) -> Vec<DateTimePart> {
fn push_lit(parts: &mut Vec<DateTimePart>, text: &str) {
if text.is_empty() {
return;
}
if let Some(last) = parts.last_mut() {
if last.kind == DateTimePartType::Literal {
last.value.push_str(text);
return;
}
}
parts.push(DateTimePart {
kind: DateTimePartType::Literal,
value: String::from(text),
});
}
let c: Vec<char> = pattern.chars().collect();
let mut parts: Vec<DateTimePart> = Vec::new();
let mut i = 0;
while i < c.len() {
let ch = c[i];
if ch == '\'' {
i += 1;
if i < c.len() && c[i] == '\'' {
push_lit(&mut parts, "'");
i += 1;
continue;
}
let mut lit = String::new();
while i < c.len() && c[i] != '\'' {
lit.push(c[i]);
i += 1;
}
i += 1; push_lit(&mut parts, &lit);
} else if ch.is_ascii_alphabetic() {
let start = i;
while i < c.len() && c[i] == ch {
i += 1;
}
let val = field(ch, i - start, dt, s);
if !val.is_empty() {
parts.push(DateTimePart {
kind: part_type(ch),
value: val,
});
}
} else {
let mut buf = [0u8; 4];
push_lit(&mut parts, ch.encode_utf8(&mut buf));
i += 1;
}
}
parts
}
fn render(pattern: &str, dt: &DateTime, s: &CalendarSpec) -> String {
let mut out = String::new();
for part in render_parts(pattern, dt, s) {
out.push_str(&part.value);
}
out
}
fn spec(lang: &str) -> CalendarSpec {
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
loop {
if let Some(s) = calendar_spec(&norm[..end]) {
return s;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => return calendar_spec("en").expect("root calendar present"),
}
}
}
#[must_use]
pub fn format_date(lang: &str, dt: &DateTime, style: DateStyle) -> String {
let s = spec(lang);
render(s.date[style.idx()], dt, &s)
}
#[must_use]
pub fn format_time(lang: &str, dt: &DateTime, style: DateStyle) -> String {
let s = spec(lang);
render(s.time[style.idx()], dt, &s)
}
#[must_use]
pub fn format_skeleton(lang: &str, dt: &DateTime, skeleton: &str) -> String {
let s = spec(lang);
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
let pattern = loop {
if let Some(p) = crate::cldr::skeleton_pattern(&norm[..end], skeleton) {
break p;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break crate::cldr::skeleton_pattern("en", skeleton).unwrap_or(s.date[2]),
}
};
render(pattern, dt, &s)
}
fn render_alt(
cal: &crate::cldr::AltCalSpec,
style: DateStyle,
year: i64,
month: i64,
day: i64,
jdn: i64,
greg: &CalendarSpec,
) -> String {
let wd = ((jdn.rem_euclid(7) + 1) % 7) as usize; let c: Vec<char> = cal.date[style.idx()].chars().collect();
let mut out = String::new();
let mut i = 0;
while i < c.len() {
let ch = c[i];
if ch == '\'' {
i += 1;
if i < c.len() && c[i] == '\'' {
out.push('\'');
i += 1;
continue;
}
while i < c.len() && c[i] != '\'' {
out.push(c[i]);
i += 1;
}
i += 1;
} else if ch.is_ascii_alphabetic() {
let start = i;
while i < c.len() && c[i] == ch {
i += 1;
}
let (n, m) = (i - start, month as usize);
let mi = m.clamp(1, 12) - 1; match ch {
'y' | 'Y' => out.push_str(&year.to_string()),
'M' | 'L' => match n {
1 => out.push_str(&m.to_string()),
2 => out.push_str(&two(m as i64)),
3 => out.push_str(cal.months_abbr[mi]),
_ => out.push_str(cal.months_wide[mi]),
},
'd' => out.push_str(&day.to_string()),
'E' | 'e' | 'c' => out.push_str(if n >= 4 {
greg.days_wide[wd]
} else {
greg.days_abbr[wd]
}),
'G' => out.push_str(cal.era),
_ => {}
}
} else {
out.push(ch);
i += 1;
}
}
out
}
fn alt_spec(lang: &str, f: fn(&str) -> Option<crate::cldr::AltCalSpec>) -> crate::cldr::AltCalSpec {
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
loop {
if let Some(s) = f(&norm[..end]) {
return s;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => return f("en").expect("root calendar present"),
}
}
}
#[must_use]
pub fn format_islamic_date(
lang: &str,
year: i64,
month: i64,
day: i64,
style: DateStyle,
) -> String {
let cal = alt_spec(lang, crate::cldr::islamic_spec);
let jdn = crate::calendar::islamic_to_jdn(year, month, day);
render_alt(&cal, style, year, month, day, jdn, &spec(lang))
}
#[must_use]
pub fn format_persian_date(
lang: &str,
year: i64,
month: i64,
day: i64,
style: DateStyle,
) -> String {
let cal = alt_spec(lang, crate::cldr::persian_spec);
let jdn = crate::calendar::persian_to_jdn(year, month, day);
render_alt(&cal, style, year, month, day, jdn, &spec(lang))
}
#[must_use]
pub fn format_gmt_offset(lang: &str, offset_minutes: i32) -> String {
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
let tz = loop {
if let Some(t) = crate::cldr::tz_spec(&norm[..end]) {
break t;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break crate::cldr::tz_spec("en").expect("root tz present"),
}
};
if offset_minutes == 0 {
return String::from(tz.zero);
}
let (pos, neg) = tz.hour.split_once(';').unwrap_or((tz.hour, tz.hour));
let sub = if offset_minutes >= 0 { pos } else { neg };
let (h, m) = (
offset_minutes.unsigned_abs() / 60,
offset_minutes.unsigned_abs() % 60,
);
let body = sub
.replace("HH", &alloc::format!("{h:02}"))
.replace("mm", &alloc::format!("{m:02}"));
tz.gmt.replace("{0}", &body)
}
#[must_use]
pub fn format_datetime(
lang: &str,
dt: &DateTime,
date_style: DateStyle,
time_style: DateStyle,
) -> String {
let s = spec(lang);
let date = render(s.date[date_style.idx()], dt, &s);
let time = render(s.time[time_style.idx()], dt, &s);
s.datetime[date_style.idx()]
.replace("{1}", &date)
.replace("{0}", &time)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Numeric2Digit {
Numeric,
TwoDigit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MonthStyle {
Numeric,
TwoDigit,
Long,
Short,
Narrow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NameStyle {
Long,
Short,
Narrow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeZoneNameStyle {
Long,
Short,
ShortOffset,
LongOffset,
ShortGeneric,
LongGeneric,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HourCycle {
H11,
H12,
H23,
H24,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DateTimeFormatOptions {
pub weekday: Option<NameStyle>,
pub era: Option<NameStyle>,
pub year: Option<Numeric2Digit>,
pub month: Option<MonthStyle>,
pub day: Option<Numeric2Digit>,
pub hour: Option<Numeric2Digit>,
pub minute: Option<Numeric2Digit>,
pub second: Option<Numeric2Digit>,
pub fractional_second_digits: Option<u8>,
pub day_period: Option<NameStyle>,
pub time_zone_name: Option<TimeZoneNameStyle>,
pub hour_cycle: Option<HourCycle>,
pub hour12: Option<bool>,
pub date_style: Option<DateStyle>,
pub time_style: Option<DateStyle>,
pub tz_offset_minutes: Option<i32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DateTimeFormatError {
ConflictingOptions,
}
fn normalize_lang(lang: &str) -> String {
lang.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect()
}
fn hour_letter(o: &DateTimeFormatOptions) -> char {
match (o.hour_cycle, o.hour12) {
(Some(HourCycle::H11 | HourCycle::H12), _) => 'h',
(Some(HourCycle::H23 | HourCycle::H24), _) => 'H',
(None, Some(true)) => 'h',
(None, Some(false)) => 'H',
(None, None) => 'H',
}
}
fn build_date_skeleton(o: &DateTimeFormatOptions) -> (String, bool) {
let mut sk = String::new();
if o.era.is_some() {
sk.push('G');
}
if o.year.is_some() {
sk.push('y');
}
if let Some(m) = o.month {
let c = match m {
MonthStyle::Numeric | MonthStyle::TwoDigit => 1,
MonthStyle::Short => 3,
MonthStyle::Long => 4,
MonthStyle::Narrow => 5,
};
for _ in 0..c {
sk.push('M');
}
}
if o.weekday.is_some() {
sk.push('E');
}
if o.day.is_some() {
sk.push('d');
}
let any = o.era.is_some()
|| o.year.is_some()
|| o.month.is_some()
|| o.weekday.is_some()
|| o.day.is_some();
(sk, any)
}
fn build_time_skeleton(o: &DateTimeFormatOptions) -> (String, bool) {
let mut sk = String::new();
if o.hour.is_some() {
sk.push(hour_letter(o));
}
if o.minute.is_some() {
sk.push('m');
}
if o.second.is_some() {
sk.push('s');
}
let any = o.hour.is_some() || o.minute.is_some() || o.second.is_some();
(sk, any)
}
fn resolve_one(lang: &str, s: &CalendarSpec, skeleton: &str) -> String {
let norm = normalize_lang(lang);
let mut end = norm.len();
loop {
if let Some(p) = crate::cldr::skeleton_pattern(&norm[..end], skeleton) {
return String::from(p);
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => {
return crate::cldr::skeleton_pattern("en", skeleton)
.map_or_else(|| String::from(s.date[2]), String::from)
}
}
}
}
fn set_field(pattern: &str, from: &[char], to: char, count: usize) -> String {
let chars: Vec<char> = pattern.chars().collect();
let mut out = String::new();
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
if ch == '\'' {
out.push(ch);
i += 1;
if i < chars.len() && chars[i] == '\'' {
out.push('\'');
i += 1;
continue;
}
while i < chars.len() && chars[i] != '\'' {
out.push(chars[i]);
i += 1;
}
if i < chars.len() {
out.push('\'');
i += 1;
}
} else if from.contains(&ch) {
while i < chars.len() && chars[i] == ch {
i += 1;
}
for _ in 0..count {
out.push(to);
}
} else {
out.push(ch);
i += 1;
}
}
out
}
fn patch_widths(pattern: &str, o: &DateTimeFormatOptions) -> String {
let mut p = String::from(pattern);
if let Some(y) = o.year {
p = set_field(
&p,
&['y'],
'y',
if y == Numeric2Digit::TwoDigit { 2 } else { 1 },
);
}
if let Some(m) = o.month {
let c = match m {
MonthStyle::Numeric => 1,
MonthStyle::TwoDigit => 2,
MonthStyle::Short => 3,
MonthStyle::Long => 4,
MonthStyle::Narrow => 5,
};
p = set_field(&p, &['M', 'L'], 'M', c);
}
if let Some(d) = o.day {
p = set_field(
&p,
&['d'],
'd',
if d == Numeric2Digit::TwoDigit { 2 } else { 1 },
);
}
if let Some(w) = o.weekday {
let c = match w {
NameStyle::Long => 4,
NameStyle::Short => 3,
NameStyle::Narrow => 5,
};
p = set_field(&p, &['E', 'e', 'c'], 'E', c);
}
if let Some(e) = o.era {
let c = match e {
NameStyle::Long => 4,
NameStyle::Short => 1,
NameStyle::Narrow => 5,
};
p = set_field(&p, &['G'], 'G', c);
}
if let Some(h) = o.hour {
let letter = hour_letter(o);
let c = if h == Numeric2Digit::TwoDigit { 2 } else { 1 };
p = set_field(&p, &['h', 'H', 'k', 'K'], letter, c);
}
if let Some(mi) = o.minute {
p = set_field(
&p,
&['m'],
'm',
if mi == Numeric2Digit::TwoDigit { 2 } else { 1 },
);
}
if let Some(se) = o.second {
p = set_field(
&p,
&['s'],
's',
if se == Numeric2Digit::TwoDigit { 2 } else { 1 },
);
}
p
}
fn inject_fractional(pattern: &str, lang: &str, n: u8) -> String {
let dec = crate::cldr::number_spec(lang).map_or(".", |s| s.decimal);
let chars: Vec<char> = pattern.chars().collect();
let mut out = String::new();
let mut i = 0;
let mut done = false;
while i < chars.len() {
let ch = chars[i];
if ch == '\'' {
out.push(ch);
i += 1;
while i < chars.len() && chars[i] != '\'' {
out.push(chars[i]);
i += 1;
}
if i < chars.len() {
out.push('\'');
i += 1;
}
} else if ch == 's' && !done {
while i < chars.len() && chars[i] == 's' {
out.push('s');
i += 1;
}
out.push_str(dec);
for _ in 0..n {
out.push('S');
}
done = true;
} else {
out.push(ch);
i += 1;
}
}
out
}
fn strip_fields(pattern: &str, keep: &[char]) -> String {
enum Tok {
Field(String),
Lit(String),
}
let chars: Vec<char> = pattern.chars().collect();
let mut toks: Vec<Tok> = Vec::new();
let mut i = 0;
let mut lit = String::new();
let flush = |lit: &mut String, toks: &mut Vec<Tok>| {
if !lit.is_empty() {
toks.push(Tok::Lit(core::mem::take(lit)));
}
};
while i < chars.len() {
let ch = chars[i];
if ch == '\'' {
i += 1;
if i < chars.len() && chars[i] == '\'' {
lit.push('\'');
i += 1;
continue;
}
while i < chars.len() && chars[i] != '\'' {
lit.push(chars[i]);
i += 1;
}
i += 1;
} else if ch.is_ascii_alphabetic() {
flush(&mut lit, &mut toks);
let start = i;
while i < chars.len() && chars[i] == ch {
i += 1;
}
let run: String = chars[start..i].iter().collect();
if keep.contains(&ch) {
toks.push(Tok::Field(run));
}
} else {
lit.push(ch);
i += 1;
}
}
flush(&mut lit, &mut toks);
while matches!(toks.first(), Some(Tok::Lit(_))) {
toks.remove(0);
}
while matches!(toks.last(), Some(Tok::Lit(_))) {
toks.pop();
}
let mut out = String::new();
let mut prev_lit = false;
for t in toks {
match t {
Tok::Field(f) => {
out.push_str(&f);
prev_lit = false;
}
Tok::Lit(l) => {
if prev_lit {
continue;
}
out.push_str(&l);
prev_lit = true;
}
}
}
out
}
fn resolve_pattern(
lang: &str,
s: &CalendarSpec,
o: &DateTimeFormatOptions,
) -> Result<String, DateTimeFormatError> {
let has_components = o.weekday.is_some()
|| o.era.is_some()
|| o.year.is_some()
|| o.month.is_some()
|| o.day.is_some()
|| o.hour.is_some()
|| o.minute.is_some()
|| o.second.is_some()
|| o.fractional_second_digits.is_some()
|| o.day_period.is_some();
if o.date_style.is_some() || o.time_style.is_some() {
if has_components {
return Err(DateTimeFormatError::ConflictingOptions);
}
let pat = match (o.date_style, o.time_style) {
(Some(d), Some(t)) => s.datetime[d.idx()]
.replace("{1}", s.date[d.idx()])
.replace("{0}", s.time[t.idx()]),
(Some(d), None) => String::from(s.date[d.idx()]),
(None, Some(t)) => String::from(s.time[t.idx()]),
(None, None) => unreachable!(),
};
return Ok(pat);
}
let (date_sk, mut want_date) = build_date_skeleton(o);
let (time_sk, want_time) = build_time_skeleton(o);
let date_sk = if !want_date && !want_time {
want_date = true;
String::from("yMd")
} else {
date_sk
};
let date_pat = if want_date {
Some(resolve_one(lang, s, &date_sk))
} else {
None
};
let time_pat = if want_time {
Some(resolve_one(lang, s, &time_sk))
} else {
None
};
let mut combined = match (date_pat, time_pat) {
(Some(d), Some(t)) => s.datetime[2].replace("{1}", &d).replace("{0}", &t),
(Some(d), None) => d,
(None, Some(t)) => t,
(None, None) => String::from(s.date[2]),
};
if has_components {
let mut keep: Vec<char> = Vec::new();
if o.year.is_some() {
keep.extend(['y', 'Y', 'u', 'U', 'r']);
}
if o.month.is_some() {
keep.extend(['M', 'L']);
}
if o.day.is_some() {
keep.extend(['d', 'D', 'F', 'g']);
}
if o.weekday.is_some() {
keep.extend(['E', 'e', 'c']);
}
if o.era.is_some() {
keep.push('G');
}
if o.hour.is_some() {
keep.extend(['h', 'H', 'k', 'K']);
}
if o.minute.is_some() {
keep.push('m');
}
if o.second.is_some() {
keep.push('s');
}
if o.day_period.is_some() || (o.hour.is_some() && hour_letter(o) == 'h') {
keep.extend(['a', 'b', 'B']);
}
combined = strip_fields(&combined, &keep);
}
combined = patch_widths(&combined, o);
if let Some(n) = o.fractional_second_digits {
combined = inject_fractional(&combined, lang, n);
}
Ok(combined)
}
fn zone_string(lang: &str, style: TimeZoneNameStyle, offset: i32) -> String {
let long = format_gmt_offset(lang, offset);
match style {
TimeZoneNameStyle::LongOffset
| TimeZoneNameStyle::Long
| TimeZoneNameStyle::LongGeneric => long,
TimeZoneNameStyle::ShortOffset
| TimeZoneNameStyle::Short
| TimeZoneNameStyle::ShortGeneric => {
if let Some(pos) = long.find(['+', '-', '\u{2212}']) {
let (head, tail) = long.split_at(pos + 1);
let trimmed = tail.strip_prefix('0').unwrap_or(tail);
alloc::format!("{head}{trimmed}")
} else {
long
}
}
}
}
pub fn format_to_parts(
lang: &str,
dt: &DateTime,
opts: &DateTimeFormatOptions,
) -> Result<Vec<DateTimePart>, DateTimeFormatError> {
let s = spec(lang);
let pattern = resolve_pattern(lang, &s, opts)?;
let mut parts = render_parts(&pattern, dt, &s);
if let (Some(style), Some(off)) = (opts.time_zone_name, opts.tz_offset_minutes) {
parts.push(DateTimePart {
kind: DateTimePartType::Literal,
value: String::from(" "),
});
parts.push(DateTimePart {
kind: DateTimePartType::TimeZoneName,
value: zone_string(lang, style, off),
});
}
Ok(parts)
}
pub fn format_options(
lang: &str,
dt: &DateTime,
opts: &DateTimeFormatOptions,
) -> Result<String, DateTimeFormatError> {
let parts = format_to_parts(lang, dt, opts)?;
let mut out = String::new();
for p in parts {
out.push_str(&p.value);
}
Ok(out)
}
#[must_use]
pub fn format_skeleton_to_parts(lang: &str, dt: &DateTime, skeleton: &str) -> Vec<DateTimePart> {
let s = spec(lang);
let norm = normalize_lang(lang);
let mut end = norm.len();
let pattern = loop {
if let Some(p) = crate::cldr::skeleton_pattern(&norm[..end], skeleton) {
break String::from(p);
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => {
break crate::cldr::skeleton_pattern("en", skeleton)
.map_or_else(|| String::from(s.date[2]), String::from)
}
}
};
render_parts(&pattern, dt, &s)
}