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};
#[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,
) -> Result<Value, 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(&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 {:?}",
other.data_type()
),
});
}
};
let chars: Vec<char> = s.chars().collect();
let len = chars.len() as i64;
if 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()));
}
Ok(Value::Text(chars[start..end].iter().collect()))
}
pub(super) fn string_pad(args: &[Value], is_left: bool, fn_name: &str) -> Result<Value, 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(&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 {:?}",
other.data_type()
),
});
}
};
let fill = if args.len() == 3 {
value_to_format_text(&args[2])
} else {
String::from(" ")
};
if target <= 0 {
return Ok(Value::Text(String::new()));
}
let target = target as usize;
let s_chars: Vec<char> = s.chars().collect();
if s_chars.len() >= target {
return Ok(Value::Text(s_chars[..target].iter().collect()));
}
if fill.is_empty() {
return Ok(Value::Text(s));
}
let pad_needed = target - s_chars.len();
let fill_chars: Vec<char> = fill.chars().collect();
let mut padding = String::with_capacity(pad_needed * 4);
for i in 0..pad_needed {
padding.push(fill_chars[i % fill_chars.len()]);
}
if is_left {
Ok(Value::Text(padding + &s))
} else {
Ok(Value::Text(s + &padding))
}
}
pub(super) fn string_trim(
args: &[Value],
side: TrimSide,
fn_name: &str,
) -> Result<Value, EvalError> {
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()))
}
pub(super) fn format_string(args: &[Value]) -> Result<Value, EvalError> {
if args.is_empty() {
return Err(EvalError::TypeMismatch {
detail: "format() takes at least 1 arg (format string)".into(),
});
}
let fmt = match &args[0] {
Value::Text(s) => s.clone(),
Value::Null => return Ok(Value::Null),
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"format(): first arg must be text, got {:?}",
other.data_type()
),
});
}
};
let arg_values = &args[1..];
let mut out = String::new();
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 digit_buf = String::new();
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
digit_buf.push(d);
chars.next();
} else {
break;
}
}
if !digit_buf.is_empty() && matches!(chars.peek(), Some(&'$')) {
chars.next(); explicit_pos =
Some(
digit_buf
.parse::<usize>()
.map_err(|_| EvalError::TypeMismatch {
detail: format!("format(): invalid arg position {digit_buf:?}"),
})?,
);
digit_buf.clear();
}
let spec = match chars.next() {
Some(c) => c,
None => {
return Err(EvalError::TypeMismatch {
detail: "format(): trailing `%` with no specifier".into(),
});
}
};
let _ = digit_buf;
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).cloned().unwrap_or(Value::Null);
match spec {
's' => match arg {
Value::Null => {} v => out.push_str(&value_to_format_text(&v)),
},
'I' => match arg {
Value::Null => {
return Err(EvalError::TypeMismatch {
detail: "format(): NULL is not a valid identifier (%I)".into(),
});
}
v => {
let s = value_to_format_text(&v);
out.push('"');
for ch in s.chars() {
if ch == '"' {
out.push('"');
out.push('"');
} else {
out.push(ch);
}
}
out.push('"');
}
},
'L' => match arg {
Value::Null => out.push_str("NULL"),
v => {
let s = value_to_format_text(&v);
out.push('\'');
for ch in s.chars() {
if ch == '\'' {
out.push('\'');
out.push('\'');
} else {
out.push(ch);
}
}
out.push('\'');
}
},
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"format(): unknown specifier '%{other}' \
(v7.17 supports %s %I %L %%)"
),
});
}
}
}
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::Text(_) => "text",
Value::Bool(_) => "boolean",
Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => "vector",
Value::Numeric { .. } => "numeric",
Value::Date(_) => "date",
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::TsVector(_) => "tsvector",
Value::TsQuery(_) => "tsquery",
Value::Uuid(_) => "uuid",
Value::Null => "unknown",
_ => "unknown",
}
}
pub(super) fn value_to_format_text(v: &Value) -> String {
match v {
Value::Text(s) | Value::Json(s) => s.clone(),
Value::SmallInt(n) => n.to_string(),
Value::Int(n) => n.to_string(),
Value::BigInt(n) => n.to_string(),
Value::Float(x) => format!("{x}"),
Value::Bool(b) => {
if *b {
"t".into()
} else {
"f".into()
}
}
Value::Null => String::new(),
other => format!("{other:?}"),
}
}
pub(super) fn to_char(args: &[Value]) -> Result<Value, 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 {:?}",
args[1].data_type()
),
});
};
let (days, day_micros) = match &args[0] {
Value::Date(d) => (*d, 0_i64),
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 DATE or TIMESTAMP, got {:?}",
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 mut out = String::with_capacity(fmt.len() + 8);
let bytes = fmt.as_bytes();
let mut i = 0;
while i < bytes.len() {
let rest = &bytes[i..];
if rest.starts_with(b"YYYY") {
let _ = write!(out, "{y:04}");
i += 4;
} else if rest.starts_with(b"YY") {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let yy = (y.rem_euclid(100)) as u32;
let _ = write!(out, "{yy:02}");
i += 2;
} else if rest.starts_with(b"Month") {
out.push_str(MONTH_FULL[(mo - 1) as usize]);
i += 5;
} else if rest.starts_with(b"Mon") {
out.push_str(MONTH_ABBR[(mo - 1) as usize]);
i += 3;
} else if rest.starts_with(b"MM") {
let _ = write!(out, "{mo:02}");
i += 2;
} else if rest.starts_with(b"DD") {
let _ = write!(out, "{d:02}");
i += 2;
} else if rest.starts_with(b"HH24") {
let _ = write!(out, "{hh24:02}");
i += 4;
} else if rest.starts_with(b"HH12") {
let _ = write!(out, "{hh12:02}");
i += 4;
} else if rest.starts_with(b"MI") {
let _ = write!(out, "{mi:02}");
i += 2;
} else if rest.starts_with(b"SS") {
let _ = write!(out, "{ss:02}");
i += 2;
} else if rest.starts_with(b"MS") {
let _ = write!(out, "{ms:03}");
i += 2;
} else if rest.starts_with(b"US") {
let _ = write!(out, "{us:06}");
i += 2;
} else if rest.starts_with(b"AM") || rest.starts_with(b"PM") {
out.push_str(ampm);
i += 2;
} else {
out.push(bytes[i] as char);
i += 1;
}
}
Ok(Value::Text(out))
}