use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use spg_storage::Value;
use super::{EvalError, MONTH_ABBR, MONTH_FULL, civil_from_days, days_from_civil};
const DAY_FULL: [&str; 7] = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
const DAY_ABBR: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const MONTH_ROMAN: [&str; 12] = [
"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII",
];
fn cased_name(canonical: &str, upper: bool, lower: bool, blank_to: Option<usize>) -> String {
let mut s = if upper {
canonical.to_ascii_uppercase()
} else if lower {
canonical.to_ascii_lowercase()
} else {
canonical.to_string()
};
if let Some(width) = blank_to {
while s.len() < width {
s.push(' ');
}
}
s
}
#[derive(Debug, Clone, Copy)]
pub(super) enum TrimSide {
Left,
Right,
Both,
}
pub(super) fn string_left_right(
args: &[Value<'_>],
is_left: bool,
fn_name: &str,
mysql: bool,
) -> Result<Value<'static>, EvalError> {
if args.len() != 2 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("{fn_name}() takes 2 args, got {}", args.len()),
});
}
if args.iter().any(|v| matches!(v, Value::Null)) {
return Ok(Value::Null);
}
let s = value_to_format_text_ref(&args[0]);
let n = match &args[1] {
Value::SmallInt(x) => i64::from(*x),
Value::Int(x) => i64::from(*x),
Value::BigInt(x) => *x,
other => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"{fn_name}(): n must be integer, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let len = if is_left && n > 0 {
i64::MAX
} else {
s.chars().count() as i64
};
if n == 0 || (mysql && n < 0) {
return Ok(Value::text(String::new()));
}
let (start, end) = if is_left {
if n > 0 {
(0usize, (n.min(len)) as usize)
} else {
let drop = (-n).min(len);
(0usize, (len - drop) as usize)
}
} else if n > 0 {
let start = (len - n).max(0);
(start as usize, len as usize)
} else {
let drop = (-n).min(len);
(drop as usize, len as usize)
};
if start >= end {
return Ok(Value::text(String::new()));
}
let mut byte_start = s.len();
let mut byte_end = s.len();
for (nth, (byte, _)) in s.char_indices().enumerate() {
if nth == start {
byte_start = byte;
}
if nth == end {
byte_end = byte;
break;
}
}
if byte_start >= byte_end {
return Ok(Value::text(String::new()));
}
Ok(Value::text(alloc::string::String::from(
&s[byte_start..byte_end],
)))
}
pub(super) fn string_pad(
args: &[Value<'_>],
is_left: bool,
fn_name: &str,
) -> Result<Value<'static>, EvalError> {
if args.len() != 2 && args.len() != 3 {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("{fn_name}() takes 2 or 3 args, got {}", args.len()),
});
}
if args.iter().any(|v| matches!(v, Value::Null)) {
return Ok(Value::Null);
}
let s = value_to_format_text_ref(&args[0]);
let target = match &args[1] {
Value::SmallInt(x) => i64::from(*x),
Value::Int(x) => i64::from(*x),
Value::BigInt(x) => *x,
other => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"{fn_name}(): length must be integer, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let fill: alloc::borrow::Cow<'_, str> = if args.len() == 3 {
value_to_format_text_ref(&args[2])
} else {
alloc::borrow::Cow::Borrowed(" ")
};
if target <= 0 {
return Ok(Value::text(String::new()));
}
let target = target as usize;
let s_len = s.chars().count();
if s_len >= target {
let end = s.char_indices().nth(target).map_or(s.len(), |(i, _)| i);
return Ok(Value::text(alloc::string::String::from(&s[..end])));
}
if fill.is_empty() {
return Ok(Value::text(s.into_owned()));
}
let pad_needed = target - s_len;
let mut out = String::with_capacity(s.len() + pad_needed * 4);
if is_left {
out.extend(fill.chars().cycle().take(pad_needed));
out.push_str(&s);
} else {
out.push_str(&s);
out.extend(fill.chars().cycle().take(pad_needed));
}
Ok(Value::text(out))
}
pub(super) fn string_trim(
args: &[Value<'_>],
side: TrimSide,
fn_name: &str,
) -> Result<Value<'static>, EvalError> {
if let [Value::Bytes(b), Value::Bytes(set)] = args {
let setb: alloc::collections::BTreeSet<u8> = set.iter().copied().collect();
let mut lo = 0usize;
let mut hi = b.len();
if matches!(side, TrimSide::Left | TrimSide::Both) {
while lo < hi && setb.contains(&b[lo]) {
lo += 1;
}
}
if matches!(side, TrimSide::Right | TrimSide::Both) {
while hi > lo && setb.contains(&b[hi - 1]) {
hi -= 1;
}
}
return Ok(Value::Bytes(alloc::borrow::Cow::Owned(b[lo..hi].to_vec())));
}
let (input, chars_str) = match args {
[v] => (v.clone(), String::from(" ")),
[v, c] => (v.clone(), {
if matches!(c, Value::Null) {
return Ok(Value::Null);
}
value_to_format_text(c)
}),
_ => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("{fn_name}() takes 1 or 2 args, got {}", args.len()),
});
}
};
if matches!(input, Value::Null) {
return Ok(Value::Null);
}
let s = value_to_format_text(&input);
let charset: alloc::collections::BTreeSet<char> = chars_str.chars().collect();
let chars: Vec<char> = s.chars().collect();
let mut start = 0usize;
let mut end = chars.len();
if matches!(side, TrimSide::Left | TrimSide::Both) {
while start < end && charset.contains(&chars[start]) {
start += 1;
}
}
if matches!(side, TrimSide::Right | TrimSide::Both) {
while end > start && charset.contains(&chars[end - 1]) {
end -= 1;
}
}
Ok(Value::text(chars[start..end].iter().collect::<String>()))
}
const PG_QUOTE_KEYWORDS: &[&str] = &[
"all",
"analyse",
"analyze",
"and",
"any",
"array",
"as",
"asc",
"asymmetric",
"authorization",
"between",
"bigint",
"binary",
"bit",
"boolean",
"both",
"case",
"cast",
"char",
"character",
"check",
"coalesce",
"collate",
"collation",
"column",
"concurrently",
"constraint",
"create",
"cross",
"current_catalog",
"current_date",
"current_role",
"current_schema",
"current_time",
"current_timestamp",
"current_user",
"dec",
"decimal",
"default",
"deferrable",
"desc",
"distinct",
"do",
"else",
"end",
"except",
"exists",
"extract",
"false",
"fetch",
"float",
"for",
"foreign",
"freeze",
"from",
"full",
"grant",
"greatest",
"group",
"grouping",
"having",
"ilike",
"in",
"initially",
"inner",
"inout",
"int",
"integer",
"intersect",
"interval",
"into",
"is",
"isnull",
"join",
"json",
"json_array",
"json_arrayagg",
"json_exists",
"json_object",
"json_objectagg",
"json_query",
"json_scalar",
"json_serialize",
"json_table",
"json_value",
"lateral",
"leading",
"least",
"left",
"like",
"limit",
"localtime",
"localtimestamp",
"merge_action",
"national",
"natural",
"nchar",
"none",
"normalize",
"not",
"notnull",
"null",
"nullif",
"numeric",
"offset",
"on",
"only",
"or",
"order",
"out",
"outer",
"overlaps",
"overlay",
"placing",
"position",
"precision",
"primary",
"real",
"references",
"returning",
"right",
"row",
"select",
"session_user",
"setof",
"similar",
"smallint",
"some",
"substring",
"symmetric",
"system_user",
"table",
"tablesample",
"then",
"time",
"timestamp",
"to",
"trailing",
"treat",
"trim",
"true",
"union",
"unique",
"user",
"using",
"values",
"varchar",
"variadic",
"verbose",
"when",
"where",
"window",
"with",
"xmlattributes",
"xmlconcat",
"xmlelement",
"xmlexists",
"xmlforest",
"xmlnamespaces",
"xmlparse",
"xmlpi",
"xmlroot",
"xmlserialize",
"xmltable",
];
fn ident_needs_quotes(s: &str) -> bool {
let mut chars = s.chars();
let Some(first) = chars.next() else {
return true; };
if !(first.is_ascii_lowercase() || first == '_') {
return true;
}
if s.chars()
.any(|c| !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'))
{
return true;
}
PG_QUOTE_KEYWORDS.binary_search(&s).is_ok()
}
pub(super) fn pg_quote_ident(s: &str) -> String {
if !ident_needs_quotes(s) {
return s.to_string();
}
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
if ch == '"' {
out.push('"');
}
out.push(ch);
}
out.push('"');
out
}
pub(super) fn pg_quote_literal(s: &str) -> String {
let has_backslash = s.contains('\\');
let mut out = String::with_capacity(s.len() + 4);
if has_backslash {
out.push('E');
}
out.push('\'');
for ch in s.chars() {
match ch {
'\'' => out.push_str("''"),
'\\' => out.push_str("\\\\"),
_ => out.push(ch),
}
}
out.push('\'');
out
}
pub(super) fn format_string(
args: &[Value<'_>],
style: &super::format::RenderStyle,
) -> Result<Value<'static>, EvalError> {
if args.is_empty() {
return Err(EvalError::TypeMismatch {
detail: "format() takes at least 1 arg (format string)".into(),
});
}
let fmt: &str = match &args[0] {
Value::Text(s) => s.as_ref(),
Value::Null => return Ok(Value::Null),
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"format(): first arg must be text, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let arg_values = &args[1..];
let mut out = String::with_capacity(fmt.len() + 16 * arg_values.len().max(1));
let mut chars = fmt.chars().peekable();
let mut implicit_cursor: usize = 0;
while let Some(c) = chars.next() {
if c != '%' {
out.push(c);
continue;
}
let mut explicit_pos: Option<usize> = None;
let mut digits: usize = 0;
let mut ndigits = 0usize;
let mut digits_overflowed = false;
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
match digits
.checked_mul(10)
.and_then(|n| n.checked_add(d as usize - '0' as usize))
{
Some(n) => digits = n,
None => digits_overflowed = true,
}
ndigits += 1;
chars.next();
} else {
break;
}
}
let mut have_width = false;
let mut width_digits: usize = 0;
if ndigits > 0 && matches!(chars.peek(), Some(&'$')) {
chars.next(); if digits_overflowed {
return Err(EvalError::TypeMismatch {
detail: String::from("format(): invalid arg position"),
});
}
explicit_pos = Some(digits);
} else if ndigits > 0 {
have_width = true;
width_digits = if digits_overflowed { 0 } else { digits };
}
let mut left_justify = false;
let mut width_from_arg = false;
if !have_width {
if matches!(chars.peek(), Some(&'-')) {
chars.next();
left_justify = true;
}
if matches!(chars.peek(), Some(&'*')) {
chars.next();
width_from_arg = true;
} else {
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
width_digits = width_digits
.saturating_mul(10)
.saturating_add(d as usize - '0' as usize);
chars.next();
} else {
break;
}
}
}
}
let width: usize = if width_from_arg {
let w_arg = arg_values.get(implicit_cursor);
implicit_cursor += 1;
let w = match w_arg {
Some(Value::SmallInt(n)) => i64::from(*n),
Some(Value::Int(n)) => i64::from(*n),
Some(Value::BigInt(n)) => *n,
_ => 0,
};
if w < 0 {
left_justify = true;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let uw = w.unsigned_abs() as usize;
uw
} else {
width_digits
};
let spec = match chars.next() {
Some(c) => c,
None => {
return Err(EvalError::TypeMismatch {
detail: "format(): trailing `%` with no specifier".into(),
});
}
};
if spec == '%' {
out.push('%');
continue;
}
let arg_index = match explicit_pos {
Some(p) => p.saturating_sub(1),
None => {
let i = implicit_cursor;
implicit_cursor += 1;
i
}
};
let arg = arg_values.get(arg_index);
let converted: alloc::borrow::Cow<'_, str> = match spec {
's' => match arg {
None | Some(Value::Null) => alloc::borrow::Cow::Borrowed(""),
Some(Value::Text(s)) => alloc::borrow::Cow::Borrowed(s.as_ref()),
Some(v) => alloc::borrow::Cow::Owned(value_to_format_text_styled(v, style)),
},
'I' => match arg {
None | Some(Value::Null) => {
return Err(EvalError::TypeMismatch {
detail: "format(): NULL is not a valid identifier (%I)".into(),
});
}
Some(v) => alloc::borrow::Cow::Owned(pg_quote_ident(&value_to_format_text_styled(
v, style,
))),
},
'L' => match arg {
None | Some(Value::Null) => alloc::borrow::Cow::Borrowed("NULL"),
Some(v) => {
let s = value_to_format_text_styled(v, style);
let mut q = String::with_capacity(s.len() + 2);
q.push('\'');
for ch in s.chars() {
if ch == '\'' {
q.push('\'');
}
q.push(ch);
}
q.push('\'');
alloc::borrow::Cow::Owned(q)
}
},
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"format(): unknown specifier '%{other}' \
(supports %s %I %L %%)"
),
});
}
};
let vis_len = converted.chars().count();
if vis_len < width {
let pad = " ".repeat(width - vis_len);
if left_justify {
out.push_str(&converted);
out.push_str(&pad);
} else {
out.push_str(&pad);
out.push_str(&converted);
}
} else {
out.push_str(&converted);
}
}
Ok(Value::text(out))
}
pub(super) fn pg_typeof_name(v: &Value) -> &'static str {
match v {
Value::SmallInt(_) => "smallint",
Value::Int(_) => "integer",
Value::BigInt(_) => "bigint",
Value::Float(_) => "double precision",
Value::Real(_) => "real",
Value::Text(_) => "text",
Value::Bool(_) => "boolean",
Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => "vector",
Value::Numeric { .. } | Value::NumericBig(_) => "numeric",
Value::Date(_) => "date",
Value::Time(_) => "time without time zone",
Value::TimeTz { .. } => "time with time zone",
Value::Timestamp(_) => "timestamp without time zone",
Value::Interval { .. } => "interval",
Value::Json(_) => {
"json"
}
Value::Bytes(_) => "bytea",
Value::TextArray(_) => "text[]",
Value::IntArray(_) => "integer[]",
Value::BigIntArray(_) => "bigint[]",
Value::SmallIntArray(_) => "smallint[]",
Value::NumericArray(_) => "numeric[]",
Value::FloatArray(_) => "double precision[]",
Value::IntArray2D(_) => "integer[]",
Value::BigIntArray2D(_) => "bigint[]",
Value::TextArray2D(_) => "text[]",
Value::BoolArray2D(_) => "boolean[]",
Value::TsVector(_) => "tsvector",
Value::TsQuery(_) => "tsquery",
Value::Uuid(_) => "uuid",
Value::BitString { .. } => "bit varying",
Value::Money(_) => "money",
Value::Inet { .. } => "inet",
Value::Cidr { .. } => "cidr",
Value::Macaddr(_) => "macaddr",
Value::Macaddr8(_) => "macaddr8",
Value::PgLsn(_) => "pg_lsn",
Value::RegClass(..) => "regclass",
Value::Tid(..) => "tid",
Value::Xid(_) => "xid",
Value::Cid(_) => "cid",
Value::RegType(..) => "regtype",
Value::RegProc(_, name) => {
if name.contains('(') {
"regprocedure"
} else {
"regproc"
}
}
Value::Xml(_) => "xml",
Value::Hstore(_) => "hstore",
Value::BpChar(_) => "character",
Value::Composite(_) => "record",
Value::Point(_) => "point",
Value::Lseg(..) => "lseg",
Value::Path { .. } => "path",
Value::PgBox(..) => "box",
Value::Polygon(_) => "polygon",
Value::Line { .. } => "line",
Value::Circle { .. } => "circle",
Value::Multirange { kind, .. } => match kind {
spg_storage::RangeKind::Int4 => "int4multirange",
spg_storage::RangeKind::Int8 => "int8multirange",
spg_storage::RangeKind::Num => "nummultirange",
spg_storage::RangeKind::Ts => "tsmultirange",
spg_storage::RangeKind::TsTz => "tstzmultirange",
spg_storage::RangeKind::Date => "datemultirange",
},
Value::Range { kind, .. } => match kind {
spg_storage::RangeKind::Int4 => "int4range",
spg_storage::RangeKind::Int8 => "int8range",
spg_storage::RangeKind::Num => "numrange",
spg_storage::RangeKind::Ts => "tsrange",
spg_storage::RangeKind::TsTz => "tstzrange",
spg_storage::RangeKind::Date => "daterange",
},
Value::BoolArray(_) => "boolean[]",
Value::DateArray(_) => "date[]",
Value::TimestampArray(_) => "timestamp without time zone[]",
Value::TimestamptzArray(_) => "timestamp with time zone[]",
Value::IntervalArray(_) => "interval[]",
Value::UuidArray(_) => "uuid[]",
Value::JsonArray(_) => "json[]",
Value::JsonbArray(_) => "jsonb[]",
Value::BytesArray(_) => "bytea[]",
Value::VarcharArray(_) => "character varying[]",
Value::CharArray(_) => "character[]",
Value::MoneyArray(_) => "money[]",
Value::Null => "unknown",
_ => "unknown",
}
}
pub(super) fn value_to_format_text(v: &Value) -> String {
value_to_format_text_styled(v, &super::format::RenderStyle::default())
}
pub(super) fn value_to_format_text_styled_ref<'a>(
v: &'a Value<'a>,
style: &super::format::RenderStyle,
) -> alloc::borrow::Cow<'a, str> {
match v {
Value::Text(s) | Value::Json(s) => alloc::borrow::Cow::Borrowed(s.as_ref()),
other => alloc::borrow::Cow::Owned(value_to_format_text_styled(other, style)),
}
}
pub(super) fn value_to_format_text_ref<'a>(v: &'a Value<'a>) -> alloc::borrow::Cow<'a, str> {
match v {
Value::Text(s) | Value::Json(s) => alloc::borrow::Cow::Borrowed(s.as_ref()),
other => alloc::borrow::Cow::Owned(value_to_format_text(other)),
}
}
pub(super) fn value_to_format_text_styled(v: &Value, style: &super::format::RenderStyle) -> String {
match v {
Value::Text(s) | Value::Json(s) => s.to_string(),
Value::SmallInt(n) => n.to_string(),
Value::Int(n) => n.to_string(),
Value::BigInt(n) => n.to_string(),
Value::Float(x) => super::format::format_float_styled(*x, style),
Value::Numeric {
scaled,
scale,
kind,
} => super::format::format_numeric_kind(*kind, *scaled, *scale),
Value::Bool(b) => {
if *b {
"t".into()
} else {
"f".into()
}
}
Value::Null => String::new(),
Value::Bytes(b) if style.mysql => b.iter().map(|&x| x as char).collect(),
other => super::values::value_to_text_styled(other, style),
}
}
fn numeric_value_for_to_char(v: &Value) -> Option<f64> {
match v {
Value::SmallInt(n) => Some(f64::from(*n)),
Value::Int(n) => Some(f64::from(*n)),
#[allow(clippy::cast_precision_loss)]
Value::BigInt(n) => Some(*n as f64),
Value::Float(x) => Some(*x),
Value::Real(x) => alloc::format!("{x:.5e}").parse::<f64>().ok(),
#[allow(clippy::cast_precision_loss)]
Value::Numeric { scaled, scale, .. } => Some(
crate::eval::format_numeric(*scaled, *scale)
.parse()
.unwrap_or(f64::NAN),
),
_ => None,
}
}
fn to_char_interval(months: i64, days: i64, micros: i128, fmt: &str) -> String {
use core::fmt::Write as _;
let yyyy = months / 12;
let mm = months % 12;
let hh24 = i64::try_from(micros / 3_600_000_000).unwrap_or(0);
let mi = i64::try_from((micros / 60_000_000) % 60).unwrap_or(0);
let ss = i64::try_from((micros / 1_000_000) % 60).unwrap_or(0);
let ms = i64::try_from((micros / 1_000) % 1_000).unwrap_or(0);
let us = i64::try_from(micros % 1_000_000).unwrap_or(0);
let hh12 = match hh24.rem_euclid(12) {
0 => 12,
x => x,
};
let ampm = if hh24.rem_euclid(24) < 12 { "AM" } else { "PM" };
let pad = |v: i64, w: usize, fm: bool| -> String {
if fm {
alloc::format!("{v}")
} else if v < 0 {
alloc::format!("-{:0width$}", -v, width = w)
} else {
alloc::format!("{:0width$}", v, width = w)
}
};
let mut out = String::with_capacity(fmt.len() + 8);
let bytes = fmt.as_bytes();
let mut i = 0;
let mut fm = false;
while i < bytes.len() {
let rest = &bytes[i..];
if rest.starts_with(b"FM") {
fm = true;
i += 2;
continue;
}
if bytes[i] == b'"' {
i += 1;
let start = i;
while i < bytes.len() && bytes[i] != b'"' {
i += 1;
}
out.push_str(&fmt[start..i]);
if i < bytes.len() {
i += 1;
}
continue;
}
let (frag, consumed): (String, usize) = if rest.starts_with(b"YYYY") {
(pad(yyyy, 4, fm), 4)
} else if rest.starts_with(b"YYY") {
(pad(yyyy % 1000, 3, fm), 3)
} else if rest.starts_with(b"YY") {
(pad(yyyy % 100, 2, fm), 2)
} else if rest.starts_with(b"Y") {
(pad(yyyy % 10, 1, fm), 1)
} else if rest.starts_with(b"HH24") {
(pad(hh24, 2, fm), 4)
} else if rest.starts_with(b"HH12") {
(pad(hh12, 2, fm), 4)
} else if rest.starts_with(b"US") {
(pad(us, 6, fm), 2)
} else if rest.starts_with(b"MS") {
(pad(ms, 3, fm), 2)
} else if rest.starts_with(b"HH") {
(pad(hh12, 2, fm), 2)
} else if rest.starts_with(b"MI") {
(pad(mi, 2, fm), 2)
} else if rest.starts_with(b"SSSS") {
(alloc::format!("{}", hh24 * 3600 + mi * 60 + ss), 4)
} else if rest.starts_with(b"FF") && rest.get(2).is_some_and(u8::is_ascii_digit) {
let n = usize::from(rest[2] - b'0');
let frac = alloc::format!("{us:06}");
(frac[..n.min(6)].to_string(), 3)
} else if rest.starts_with(b"SS") {
(pad(ss, 2, fm), 2)
} else if rest.starts_with(b"DD") {
(pad(days, 2, fm), 2)
} else if rest.starts_with(b"MM") {
(pad(mm, 2, fm), 2)
} else if rest.starts_with(b"AM") || rest.starts_with(b"PM") {
(ampm.to_string(), 2)
} else {
let mut buf = String::new();
let _ = write!(buf, "{}", bytes[i] as char);
(buf, 1)
};
out.push_str(&frag);
fm = false;
i += consumed;
}
out
}
fn format_fixed_abs(x: f64, d: usize) -> String {
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
let pow = libm::pow(10.0, d as f64);
#[allow(clippy::cast_possible_truncation)]
let scaled = libm::round(x.abs() * pow) as i128;
if d == 0 {
return alloc::format!("{scaled}");
}
let unit = 10_i128.pow(d as u32);
let ip = scaled / unit;
let fp = (scaled % unit).abs();
alloc::format!("{ip}.{fp:0width$}", width = d)
}
fn to_char_v_scale(n: f64, before: &str, after: &str, fill_mode: bool) -> String {
let count_slots = |s: &str| s.chars().filter(|c| matches!(c, '9' | '0')).count();
let vdigits = count_slots(after);
let total_slots = count_slots(before) + vdigits;
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
let scaled = libm::round(n.abs() * libm::pow(10.0, vdigits as f64)) as i128;
let neg = n < 0.0 && scaled != 0;
if total_slots == 0 {
return alloc::format!("{before}{after}");
}
let digits = alloc::format!("{scaled}").len();
if digits > total_slots {
let mut out = String::new();
if !fill_mode {
out.push(' ');
}
for c in before.chars().chain(after.chars()) {
out.push(if matches!(c, '9' | '0') { '#' } else { c });
}
return out;
}
let core = if neg {
alloc::format!("-{scaled}")
} else {
alloc::format!("{scaled}")
};
if fill_mode {
core
} else {
left_pad_spaces(&core, total_slots + 1)
}
}
fn to_char_scientific(n: f64, mant_fmt: &str, fill_mode: bool) -> String {
let neg = n < 0.0 && n != 0.0;
let a = n.abs();
let exp: i32 = if a == 0.0 {
0
} else {
#[allow(clippy::cast_possible_truncation)]
{
libm::floor(libm::log10(a)) as i32
}
};
let frac_digits = mant_fmt.find(['.', 'D', 'd']).map_or(0, |dot| {
mant_fmt[dot + 1..]
.chars()
.filter(|c| matches!(c, '9' | '0'))
.count()
});
let mantissa = if a == 0.0 {
0.0
} else {
a / libm::pow(10.0, f64::from(exp))
};
let mant_str = format_fixed_abs(mantissa, frac_digits);
let sign = if neg {
"-"
} else if fill_mode {
""
} else {
" "
};
let esign = if exp < 0 { '-' } else { '+' };
alloc::format!("{sign}{mant_str}e{esign}{:02}", exp.abs())
}
fn to_char_roman(n: f64, fill_mode: bool) -> String {
#[allow(clippy::cast_possible_truncation)]
let v = libm::round(n) as i64;
if !(1..=3999).contains(&v) {
return core::iter::repeat_n('#', 15).collect();
}
const VALS: [(i64, &str); 13] = [
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
let mut out = String::new();
let mut rem = v;
for (val, sym) in VALS {
while rem >= val {
out.push_str(sym);
rem -= val;
}
}
if fill_mode {
out
} else {
alloc::format!("{out:>15}")
}
}
fn check_eeee_format(fmt: &str) -> Result<(), EvalError> {
let mut sig = String::with_capacity(fmt.len());
let mut chars = fmt.chars();
while let Some(c) = chars.next() {
match c {
'\\' => {
chars.next();
}
'"' => {
for q in chars.by_ref() {
if q == '"' {
break;
}
}
}
_ => sig.push(c.to_ascii_uppercase()),
}
}
let Some(epos) = sig.find("EEEE") else {
return Ok(());
};
let before = &sig[..epos];
if ["FM", "MI", "PL", "SG", "PR", "RN"]
.iter()
.any(|f| before.contains(f))
|| before.contains(['S', 'V', 'B'])
{
return Err(EvalError::TypeMismatch {
detail: String::from(
"\"EEEE\" is incompatible with other formats: \"EEEE\" may \
only be used together with digit and decimal point patterns",
),
});
}
let after = &sig[epos + 4..];
if after.contains([
'9', '0', '.', ',', 'D', 'G', 'L', 'V', 'S', 'M', 'I', 'P', 'R', 'N', 'B', 'F', 'H', 'E',
]) {
return Err(EvalError::TypeMismatch {
detail: String::from("\"EEEE\" must be the last pattern used"),
});
}
Ok(())
}
fn to_char_numeric(n: f64, exact: Option<(i128, u16)>, fmt: &str) -> Result<String, EvalError> {
let fill_mode = fmt.len() >= 2 && fmt[..2].eq_ignore_ascii_case("FM");
let pat = if fill_mode { &fmt[2..] } else { fmt };
let upper = pat.to_ascii_uppercase();
if upper.matches('S').count() > 1 {
return Err(EvalError::TypeMismatch {
detail: "cannot use \"S\" twice".into(),
});
}
if (upper == "TH" || upper == "RD" || upper == "ND" || upper == "ST")
&& !pat.chars().any(|c| c.is_ascii_digit())
{
return Err(EvalError::TypeMismatch {
detail: "\".\" is not a number".into(),
});
}
let keyword_len_at = |rest: &str| -> Option<usize> {
const KW4: [&str; 1] = ["EEEE"];
const KW2: [&str; 7] = ["FM", "PL", "PR", "RN", "TH", "SG", "MI"];
if rest.len() >= 4 && KW4.iter().any(|k| rest[..4].eq_ignore_ascii_case(k)) {
return Some(4);
}
if rest.len() >= 2 && KW2.iter().any(|k| rest[..2].eq_ignore_ascii_case(k)) {
return Some(2);
}
match rest.chars().next() {
Some(c)
if matches!(
c.to_ascii_uppercase(),
'S' | 'L' | 'D' | 'G' | 'V' | 'B' | 'C'
) =>
{
Some(1)
}
Some(c) if c.is_ascii_digit() || matches!(c, '.' | ',' | '$' | '%') => Some(1),
_ => None,
}
};
let mut first_kw: Option<usize> = None;
let mut last_kw_end = 0usize;
let mut scan = 0usize;
while scan < pat.len() {
if let Some(len) = keyword_len_at(&pat[scan..]) {
if first_kw.is_none() {
first_kw = Some(scan);
}
last_kw_end = scan + len;
scan += len;
} else {
scan += pat[scan..].chars().next().map_or(1, char::len_utf8);
}
}
let Some(first_kw) = first_kw else {
return Ok(String::from(pat));
};
let mut lit_prefix_len = 0usize;
while lit_prefix_len < first_kw {
let Some(c) = pat[lit_prefix_len..].chars().next() else {
break;
};
if !c.is_ascii_alphabetic() {
break;
}
lit_prefix_len += c.len_utf8();
}
let mut lit_suffix_start = pat.len();
while lit_suffix_start > last_kw_end {
let prev = pat[..lit_suffix_start]
.chars()
.next_back()
.expect("non-empty");
if !prev.is_ascii_alphabetic() {
break;
}
lit_suffix_start -= prev.len_utf8();
}
let lit_suffix_len = pat.len() - lit_suffix_start;
if lit_prefix_len >= pat.len() {
return Ok(String::from(pat));
}
if lit_prefix_len > 0 || lit_suffix_len > 0 {
let prefix = &pat[..lit_prefix_len];
let suffix = &pat[pat.len() - lit_suffix_len..];
let inner = &pat[lit_prefix_len..pat.len() - lit_suffix_len];
let inner_fmt = if fill_mode {
alloc::format!("FM{inner}")
} else {
String::from(inner)
};
return Ok(alloc::format!(
"{prefix}{}{suffix}",
to_char_numeric(n, exact, &inner_fmt)?
));
}
if pat.len() >= 2 && pat[..2].eq_ignore_ascii_case("PL") && !pat[2..].is_empty() {
let rest = &pat[2..];
let rest_fmt = if fill_mode {
alloc::format!("FM{rest}")
} else {
String::from(rest)
};
let col = if n < 0.0 { " " } else { "+" };
return Ok(alloc::format!(
"{col}{}",
to_char_numeric(n, exact, &rest_fmt)?
));
}
if pat.eq_ignore_ascii_case("RN") {
return Ok(to_char_roman(n, fill_mode));
}
if let Some(epos) = pat.to_ascii_uppercase().find("EEEE") {
return Ok(to_char_scientific(n, &pat[..epos], fill_mode));
}
if let Some(vpos) = pat.find(['V', 'v']) {
return Ok(to_char_v_scale(
n,
&pat[..vpos],
&pat[vpos + 1..],
fill_mode,
));
}
let has_pr = pat.len() >= 2 && pat[pat.len() - 2..].eq_ignore_ascii_case("PR");
let mut pat = if has_pr { &pat[..pat.len() - 2] } else { pat };
let th_suffix: Option<bool> =
if pat.len() >= 2 && pat[pat.len() - 2..].eq_ignore_ascii_case("TH") {
let upper = pat.ends_with("TH");
pat = &pat[..pat.len() - 2];
Some(upper)
} else {
None
};
let has_pct = pat.ends_with('%');
if has_pct {
pat = &pat[..pat.len() - 1];
}
let has_locale_currency = pat.starts_with(['L', 'l']);
let has_lit_currency = !has_locale_currency && pat.starts_with('$');
if has_locale_currency || has_lit_currency {
pat = &pat[1..];
}
let has_leading_sg = pat.len() >= 2 && pat[..2].eq_ignore_ascii_case("SG");
if has_leading_sg {
pat = &pat[2..];
}
let ends_kw = |p: &str, kw: &str| p.len() >= 2 && p[p.len() - 2..].eq_ignore_ascii_case(kw);
let has_mi = ends_kw(pat, "MI");
if has_mi {
pat = &pat[..pat.len() - 2];
}
let has_pl = !has_mi && ends_kw(pat, "PL");
if has_pl {
pat = &pat[..pat.len() - 2];
}
let has_sg = !has_mi && !has_pl && ends_kw(pat, "SG");
if has_sg {
pat = &pat[..pat.len() - 2];
}
let has_trailing_s = !has_sg
&& (pat.ends_with('S') || pat.ends_with('s'))
&& !pat.ends_with("SS")
&& !pat.ends_with("ss");
if has_trailing_s {
pat = &pat[..pat.len() - 1];
}
let has_dollar = pat.ends_with('$');
if has_dollar {
pat = &pat[..pat.len() - 1];
}
let trailing_sign = has_mi || has_pl || has_sg || has_trailing_s;
let has_sign_tok = !trailing_sign && pat.chars().any(|c| c == 'S' || c == 's');
let dec_pos = pat
.char_indices()
.find(|(_, c)| *c == '.' || *c == 'D' || *c == 'd')
.map(|(i, c)| (i, c.len_utf8()));
let (int_pat, frac_pat, has_decimal) = match dec_pos {
Some((i, w)) => (&pat[..i], &pat[i + w..], true),
None => (pat, "", false),
};
let is_slot = |c: char| matches!(c, '9' | '0');
let is_group = |c: char| matches!(c, ',' | 'G' | 'g');
let int_slots = int_pat.chars().filter(|c| is_slot(*c)).count();
let frac_digits = frac_pat.chars().filter(|c| is_slot(*c)).count();
let has_group = int_pat.chars().any(is_group);
let has_numeric_field = int_slots > 0 || frac_digits > 0 || has_decimal;
let sign_col = usize::from(!(has_mi || has_sg || has_trailing_s) && has_numeric_field);
let int_field_width = int_pat
.chars()
.filter(|c| is_slot(*c) || is_group(*c))
.count()
+ sign_col;
let int_slot_chars: alloc::vec::Vec<char> = int_pat.chars().filter(|c| is_slot(*c)).collect();
let units_slot = int_slot_chars.last().copied().unwrap_or('9');
let zero_pad = int_slot_chars
.iter()
.position(|c| *c == '0')
.map_or(0, |i| int_slot_chars.len() - i);
let pow = 10_i128.pow(frac_digits as u32);
#[allow(clippy::cast_possible_truncation)]
let f64_scaled = || libm::round(n.abs() * pow as f64) as i128;
let exact_scaled = exact.and_then(|(in_scaled, in_scale)| {
let abs = in_scaled.unsigned_abs();
let fd = u32::try_from(frac_digits).ok()?;
let insc = u32::from(in_scale);
let rescaled: u128 = if fd >= insc {
10_u128
.checked_pow(fd - insc)
.and_then(|m| abs.checked_mul(m))?
} else {
let divisor = 10_u128.checked_pow(insc - fd)?;
(abs / divisor) + u128::from(abs % divisor >= divisor.div_ceil(2))
};
i128::try_from(rescaled).ok()
});
let (scaled, neg) = match exact_scaled {
Some(s) => (s, exact.is_some_and(|(v, _)| v < 0) && s != 0),
None => {
let s = f64_scaled();
(s, n < 0.0 && s != 0)
}
};
let int_part = scaled / pow;
let frac_part = scaled % pow;
let value_is_zero = scaled == 0;
let sign_str: &str = if has_pl && neg && has_numeric_field {
"-"
} else if has_pr || trailing_sign {
""
} else if neg {
"-"
} else if has_sign_tok {
"+"
} else {
""
};
let int_digit_len = if int_part == 0 {
0
} else {
alloc::format!("{int_part}").len()
};
if has_numeric_field && int_digit_len > int_slots {
let mut core = String::new();
core.push_str(sign_str);
if has_group {
let mut seen_slot = false;
for c in int_pat.chars() {
match c {
'9' | '0' => {
core.push('#');
seen_slot = true;
}
',' | 'G' | 'g' => core.push(if seen_slot { ',' } else { ' ' }),
_ => {}
}
}
} else {
for _ in 0..int_slots {
core.push('#');
}
}
let mut out = if fill_mode {
core
} else {
left_pad_spaces(&core, int_field_width)
};
if has_decimal {
out.push('.');
for _ in 0..frac_digits {
out.push('#');
}
}
if has_locale_currency {
out.insert(0, ' ');
} else if has_lit_currency {
out.insert(0, '$');
}
return Ok(out);
}
let mut body = if !has_numeric_field {
String::new()
} else if int_part == 0 {
let show_zero = if fill_mode {
value_is_zero || units_slot == '0'
} else {
units_slot == '0' || !has_decimal
};
if show_zero {
"0".to_string()
} else {
String::new()
}
} else {
alloc::format!("{int_part}")
};
while !body.is_empty() && body.chars().count() < zero_pad {
body.insert(0, '0');
}
let mut out = if has_group {
let field = render_positional_groups(int_pat, &body);
if fill_mode {
alloc::format!("{sign_str}{}", field.trim_start())
} else {
left_pad_spaces(&alloc::format!("{sign_str}{field}"), int_field_width)
}
} else if fill_mode {
alloc::format!("{sign_str}{body}")
} else {
left_pad_spaces(&alloc::format!("{sign_str}{body}"), int_field_width)
};
if has_decimal {
let mut fs = alloc::format!("{frac_part:0width$}", width = frac_digits);
if fill_mode {
let keep = frac_pat.chars().filter(|c| *c == '0').count();
while fs.chars().count() > keep && fs.ends_with('0') {
fs.pop();
}
out.push('.');
out.push_str(&fs);
} else if frac_digits > 0 {
out.push('.');
out.push_str(&fs);
}
}
if has_pr && has_numeric_field {
if neg {
let trimmed = out.trim_start();
let lead = out.chars().count() - trimmed.chars().count();
out = alloc::format!("{}<{trimmed}>", " ".repeat(lead.saturating_sub(1)));
} else if !fill_mode {
out.push(' ');
}
}
if has_mi {
if neg {
out.push('-');
} else if !fill_mode {
out.push(' ');
}
} else if has_pl {
out.push(if neg { ' ' } else { '+' });
} else if has_sg || (has_trailing_s && has_numeric_field) {
out.push(if neg { '-' } else { '+' });
}
if has_dollar {
out.push('$');
}
if let Some(upper) = th_suffix {
let suf = ordinal_suffix(int_part);
if upper {
out.push_str(&suf.to_ascii_uppercase());
} else {
out.push_str(suf);
}
}
if has_pct {
out.push('%');
}
if has_leading_sg {
let sign = if neg { '-' } else { '+' };
if out.starts_with(' ') || out.starts_with('-') {
out.replace_range(..1, &sign.to_string());
} else {
out.insert(0, sign);
}
}
if has_locale_currency {
out.insert(0, ' ');
} else if has_lit_currency {
out.insert(0, '$');
}
Ok(out)
}
fn ordinal_suffix(n: i128) -> &'static str {
let n = n.unsigned_abs();
if (11..=13).contains(&(n % 100)) {
return "th";
}
match n % 10 {
1 => "st",
2 => "nd",
3 => "rd",
_ => "th",
}
}
fn left_pad_spaces(s: &str, width: usize) -> String {
let len = s.chars().count();
if len >= width {
return s.to_string();
}
let mut out = String::with_capacity(width);
for _ in 0..width - len {
out.push(' ');
}
out.push_str(s);
out
}
fn zone_hours(zone: Option<(&str, i64)>) -> String {
let secs = zone.map_or(0, |(_, off)| off / 1_000_000);
let h = secs / 3600;
alloc::format!("{}{:02}", if h < 0 { '-' } else { '+' }, h.abs())
}
fn zone_minutes(zone: Option<(&str, i64)>) -> String {
let secs = zone.map_or(0, |(_, off)| off / 1_000_000);
alloc::format!("{:02}", (secs.abs() / 60) % 60)
}
fn render_positional_groups(int_pat: &str, digits: &str) -> String {
let mut rev: alloc::vec::Vec<char> = alloc::vec::Vec::new();
let mut left = digits.chars().rev();
let mut pending: Option<char> = left.next();
for c in int_pat.chars().rev() {
match c {
'9' | '0' => {
rev.push(pending.unwrap_or(' '));
if pending.is_some() {
pending = left.next();
}
}
',' | 'G' | 'g' => rev.push(if pending.is_some() { ',' } else { ' ' }),
other => rev.push(other),
}
}
rev.iter().rev().collect()
}
fn group_thousands(int_str: &str) -> String {
let bytes: alloc::vec::Vec<char> = int_str.chars().collect();
let mut out = String::new();
let len = bytes.len();
for (idx, c) in bytes.iter().enumerate() {
if idx > 0 && (len - idx) % 3 == 0 {
out.push(',');
}
out.push(*c);
}
out
}
pub(super) fn to_char(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
to_char_in_zone(args, None)
}
pub(super) fn to_char_in_zone(
args: &[Value<'_>],
zone: Option<(&str, i64)>,
) -> Result<Value<'static>, EvalError> {
use core::fmt::Write as _;
if args.len() != 2 {
return Err(EvalError::TypeMismatch {
detail: format!("to_char() takes 2 args, got {}", args.len()),
});
}
if matches!(&args[0], Value::Null) || matches!(&args[1], Value::Null) {
return Ok(Value::Null);
}
let Value::Text(fmt) = &args[1] else {
return Err(EvalError::TypeMismatch {
detail: format!(
"to_char() needs a text format, got {}",
crate::conversions::pg_type_name_for_error_opt(args[1].data_type())
),
});
};
if let Value::Interval {
months,
days,
micros,
} = &args[0]
{
return Ok(Value::text(to_char_interval(
i64::from(*months),
i64::from(*days),
i128::from(*micros),
fmt,
)));
}
if let Some(n) = numeric_value_for_to_char(&args[0]) {
check_eeee_format(fmt)?;
let exact = match &args[0] {
Value::Numeric { scaled, scale, .. } => Some((*scaled, *scale)),
_ => None,
};
return Ok(Value::text(to_char_numeric(n, exact, fmt)?));
}
let (days, day_micros) = match &args[0] {
Value::Date(d) => (*d, 0_i64),
Value::Time(us) => (0_i32, *us),
Value::Timestamp(t) => {
let days = t.div_euclid(86_400_000_000);
(
i32::try_from(days).unwrap_or(i32::MAX),
t.rem_euclid(86_400_000_000),
)
}
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"to_char() needs a number, DATE or TIMESTAMP, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let (y, mo, d) = civil_from_days(days);
let secs = day_micros / 1_000_000;
let frac = day_micros % 1_000_000;
let hh24 = u32::try_from(secs / 3600).unwrap_or(0);
let mi = u32::try_from((secs / 60) % 60).unwrap_or(0);
let ss = u32::try_from(secs % 60).unwrap_or(0);
let hh12 = match hh24 % 12 {
0 => 12,
x => x,
};
let ampm = if hh24 < 12 { "AM" } else { "PM" };
let ms = u32::try_from(frac / 1_000).unwrap_or(0); let us = u32::try_from(frac).unwrap_or(0);
let dow_mon0 = usize::try_from((i64::from(days) + 3).rem_euclid(7)).unwrap_or(0);
let day_of_year = i64::from(days - days_from_civil(y, 1, 1)) + 1; let (iso_week, iso_year) = super::datetime::iso_week_and_year(days, y); let quarter = i64::from((mo - 1) / 3) + 1; let week_of_year = (day_of_year - 1) / 7 + 1; let week_of_month = i64::from((d - 1) / 7) + 1; let dow_sun1 = (i64::from(days) + 4).rem_euclid(7) + 1; let iso_dow = (dow_mon0 as i64) + 1; let julian = i64::from(days) + 2_440_588; let century: i64 = if y > 0 {
i64::from((y - 1) / 100) + 1
} else {
i64::from(y / 100) - 1
}; let disp_y: i64 = if y <= 0 {
1 - i64::from(y)
} else {
i64::from(y)
};
let mut out = String::with_capacity(fmt.len() + 8);
let bytes = fmt.as_bytes();
let mut i = 0;
let mut fm = false;
let mut tm = false;
let mut last_num: Option<i128> = None;
while i < bytes.len() {
let rest = &bytes[i..];
if rest.starts_with(b"FM") {
fm = true;
i += 2;
continue;
}
if rest.len() >= 2 && rest[..2].eq_ignore_ascii_case(b"TM") {
tm = true;
i += 2;
continue;
}
if bytes[i] == b'"' {
i += 1;
let start = i;
while i < bytes.len() && bytes[i] != b'"' {
i += 1;
}
out.push_str(&fmt[start..i]);
if i < bytes.len() {
i += 1; }
continue;
}
let pad = |width: usize| if fm || tm { None } else { Some(width) };
let pending_ord = last_num.take();
let mut next_num: Option<i128> = None;
macro_rules! num {
($val:expr, $width:literal) => {{
if fm {
let _ = write!(out, "{}", $val);
} else {
let _ = write!(out, "{:0width$}", $val, width = $width);
}
next_num = Some(i128::from($val));
}};
}
let mut consumed = 2usize;
if rest.starts_with(b"Y,YYY") {
out.push_str(&group_thousands(&alloc::format!("{disp_y}")));
consumed = 5;
} else if rest.starts_with(b"YYYY") {
num!(disp_y, 4);
consumed = 4;
} else if rest.starts_with(b"IYYY") {
num!(iso_year, 4);
consumed = 4;
} else if rest.starts_with(b"HH24") {
num!(hh24, 2);
consumed = 4;
} else if rest.starts_with(b"HH12") {
num!(hh12, 2);
consumed = 4;
} else if rest.starts_with(b"IYY") {
let _ = write!(out, "{:03}", iso_year.rem_euclid(1000));
consumed = 3;
} else if rest.starts_with(b"YYY") {
let _ = write!(out, "{:03}", disp_y.rem_euclid(1000));
consumed = 3;
} else if rest.starts_with(b"DDD") {
num!(day_of_year, 3);
consumed = 3;
} else if rest.starts_with(b"Month") {
out.push_str(&cased_name(
MONTH_FULL[(mo - 1) as usize],
false,
false,
pad(9),
));
consumed = 5;
} else if rest.starts_with(b"MONTH") {
out.push_str(&cased_name(
MONTH_FULL[(mo - 1) as usize],
true,
false,
pad(9),
));
consumed = 5;
} else if rest.starts_with(b"month") {
out.push_str(&cased_name(
MONTH_FULL[(mo - 1) as usize],
false,
true,
pad(9),
));
consumed = 5;
} else if rest.starts_with(b"Mon") {
out.push_str(&cased_name(
MONTH_ABBR[(mo - 1) as usize],
false,
false,
None,
));
consumed = 3;
} else if rest.starts_with(b"MON") {
out.push_str(&cased_name(
MONTH_ABBR[(mo - 1) as usize],
true,
false,
None,
));
consumed = 3;
} else if rest.starts_with(b"mon") {
out.push_str(&cased_name(
MONTH_ABBR[(mo - 1) as usize],
false,
true,
None,
));
consumed = 3;
} else if rest.starts_with(b"Day") {
out.push_str(&cased_name(DAY_FULL[dow_mon0], false, false, pad(9)));
consumed = 3;
} else if rest.starts_with(b"DAY") {
out.push_str(&cased_name(DAY_FULL[dow_mon0], true, false, pad(9)));
consumed = 3;
} else if rest.starts_with(b"day") {
out.push_str(&cased_name(DAY_FULL[dow_mon0], false, true, pad(9)));
consumed = 3;
} else if rest.starts_with(b"Dy") {
out.push_str(&cased_name(DAY_ABBR[dow_mon0], false, false, None));
} else if rest.starts_with(b"DY") {
out.push_str(&cased_name(DAY_ABBR[dow_mon0], true, false, None));
} else if rest.starts_with(b"dy") {
out.push_str(&cased_name(DAY_ABBR[dow_mon0], false, true, None));
} else if rest.starts_with(b"YY") {
let _ = write!(out, "{:02}", disp_y.rem_euclid(100));
} else if rest.starts_with(b"IW") {
num!(iso_week, 2);
} else if rest.starts_with(b"IY") {
let _ = write!(out, "{:02}", iso_year.rem_euclid(100));
} else if rest.starts_with(b"IDDD") {
let _ = write!(out, "{:03}", (iso_week - 1) * 7 + iso_dow);
consumed = 4;
} else if rest.starts_with(b"ID") {
let _ = write!(out, "{iso_dow}");
} else if rest.starts_with(b"MM") {
num!(mo, 2);
} else if rest.starts_with(b"DD") {
num!(d, 2);
} else if rest.starts_with(b"MI") {
num!(mi, 2);
} else if rest.starts_with(b"SSSSS") {
let _ = write!(out, "{}", hh24 * 3600 + mi * 60 + ss);
consumed = 5;
} else if rest.starts_with(b"SSSS") {
let _ = write!(out, "{}", hh24 * 3600 + mi * 60 + ss);
consumed = 4;
} else if rest.starts_with(b"TZH") {
out.push_str(&zone_hours(zone));
consumed = 3;
} else if rest.starts_with(b"TZM") {
out.push_str(&zone_minutes(zone));
consumed = 3;
} else if rest.starts_with(b"TZ") || rest.starts_with(b"tz") {
let name = zone.map_or_else(|| String::from("UTC"), |(n, _)| String::from(n));
out.push_str(&if rest.starts_with(b"TZ") {
name.to_uppercase()
} else {
name.to_lowercase()
});
consumed = 2;
} else if rest.starts_with(b"OF") {
out.push_str(&zone_hours(zone));
consumed = 2;
} else if rest.starts_with(b"FF") && rest.get(2).is_some_and(u8::is_ascii_digit) {
let n = usize::from(rest[2] - b'0');
let frac = alloc::format!("{us:06}");
out.push_str(&frac[..n.min(6)]);
consumed = 3;
} else if rest.starts_with(b"SS") {
num!(ss, 2);
} else if rest.starts_with(b"MS") {
let _ = write!(out, "{ms:03}");
} else if rest.starts_with(b"US") {
let _ = write!(out, "{us:06}");
} else if rest.starts_with(b"WW") {
num!(week_of_year, 2);
} else if rest.starts_with(b"CC") {
num!(century, 2);
} else if rest.starts_with(b"RM") {
out.push_str(&cased_name(
MONTH_ROMAN[(mo - 1) as usize],
true,
false,
pad(4),
));
} else if rest.starts_with(b"rm") {
out.push_str(&cased_name(
MONTH_ROMAN[(mo - 1) as usize],
false,
true,
pad(4),
));
} else if rest.starts_with(b"HH") {
num!(hh12, 2);
} else if rest.starts_with(b"A.M.") || rest.starts_with(b"P.M.") {
out.push_str(if hh24 < 12 { "A.M." } else { "P.M." });
consumed = 4;
} else if rest.starts_with(b"a.m.") || rest.starts_with(b"p.m.") {
out.push_str(if hh24 < 12 { "a.m." } else { "p.m." });
consumed = 4;
} else if rest.starts_with(b"B.C.") || rest.starts_with(b"A.D.") {
out.push_str(if i64::from(y) <= 0 { "B.C." } else { "A.D." });
consumed = 4;
} else if rest.starts_with(b"b.c.") || rest.starts_with(b"a.d.") {
out.push_str(if i64::from(y) <= 0 { "b.c." } else { "a.d." });
consumed = 4;
} else if rest.starts_with(b"AM") || rest.starts_with(b"PM") {
out.push_str(ampm);
} else if rest.starts_with(b"am") || rest.starts_with(b"pm") {
out.push_str(if hh24 < 12 { "am" } else { "pm" });
} else if rest.starts_with(b"BC") || rest.starts_with(b"AD") {
out.push_str(if i64::from(y) <= 0 { "BC" } else { "AD" });
} else if rest.starts_with(b"bc") || rest.starts_with(b"ad") {
out.push_str(if i64::from(y) <= 0 { "bc" } else { "ad" });
} else if (rest.starts_with(b"TH") || rest.starts_with(b"th")) && pending_ord.is_some() {
let suf = ordinal_suffix(pending_ord.unwrap_or(0));
if rest.starts_with(b"TH") {
out.push_str(&suf.to_ascii_uppercase());
} else {
out.push_str(suf);
}
} else if rest.starts_with(b"Y") || rest.starts_with(b"I") {
let base = if rest[0] == b'I' { iso_year } else { disp_y };
let _ = write!(out, "{}", base.rem_euclid(10));
consumed = 1;
} else if rest.starts_with(b"Q") {
let _ = write!(out, "{quarter}");
consumed = 1;
} else if rest.starts_with(b"W") {
let _ = write!(out, "{week_of_month}");
consumed = 1;
} else if rest.starts_with(b"D") {
let _ = write!(out, "{dow_sun1}");
next_num = Some(i128::from(dow_sun1));
consumed = 1;
} else if rest.starts_with(b"J") {
let _ = write!(out, "{julian}");
consumed = 1;
} else {
out.push(bytes[i] as char);
consumed = 1;
i += consumed;
continue;
}
last_num = next_num;
fm = false;
tm = false;
i += consumed;
}
Ok(Value::text(out))
}