use indexmap::IndexMap;
use unicode_general_category::{GeneralCategory, get_general_category};
use super::super::{
bind_method_params, opt_index_arg, reject_kwargs, to_index, to_len_i64, value_to_i64,
};
use crate::{
error::{EvalError, EvalResult, InterpreterError},
eval::control_flow::iterate_value,
value::{ExceptionValue, Value, shared_list},
};
fn unicode_escape_encode(s: &str) -> Vec<u8> {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (' '..='~').contains(&c) => out.push(c),
c => {
let u = c as u32;
if u <= 0xff {
out.push_str(&format!("\\x{u:02x}"));
} else if u <= 0xffff {
out.push_str(&format!("\\u{u:04x}"));
} else {
out.push_str(&format!("\\U{u:08x}"));
}
}
}
}
out.into_bytes()
}
fn encode_narrow(s: &str, max: u32, codec: &str, errors: &str) -> Result<Vec<u8>, EvalError> {
let mut out = Vec::with_capacity(s.len());
for ch in s.chars() {
let u = ch as u32;
if u <= max {
out.push(u as u8);
continue;
}
match errors {
"strict" => {
return Err(EvalError::Exception(ExceptionValue::new(
"UnicodeEncodeError",
format!("'{codec}' codec can't encode character"),
)));
}
"ignore" => {}
"replace" => out.push(b'?'),
"xmlcharrefreplace" => out.extend_from_slice(format!("&#{u};").as_bytes()),
"backslashreplace" => {
let esc = if u <= 0xff {
format!("\\x{u:02x}")
} else if u <= 0xffff {
format!("\\u{u:04x}")
} else {
format!("\\U{u:08x}")
};
out.extend_from_slice(esc.as_bytes());
}
other => {
return Err(EvalError::Exception(ExceptionValue::new(
"LookupError",
format!("unknown error handler name '{other}'"),
)));
}
}
}
Ok(out)
}
fn is_unicode_digit(c: char) -> bool {
if get_general_category(c) == GeneralCategory::DecimalNumber {
return true;
}
matches!(c,
'\u{00B2}' | '\u{00B3}' | '\u{00B9}' | '\u{1369}'..='\u{1371}' | '\u{2070}' | '\u{2074}'..='\u{2079}' | '\u{2080}'..='\u{2089}' | '\u{2460}'..='\u{2468}' | '\u{2474}'..='\u{247C}' | '\u{2488}'..='\u{2490}' | '\u{24EA}' | '\u{24F5}'..='\u{24FD}' | '\u{24FF}' | '\u{2776}'..='\u{277E}' | '\u{2780}'..='\u{2788}' | '\u{278A}'..='\u{2792}' )
}
pub(crate) fn dispatch_string_method(
s: &str,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> EvalResult {
if !kwargs.is_empty()
&& !matches!(method, "split" | "rsplit" | "encode" | "expandtabs" | "splitlines")
{
reject_kwargs(method, kwargs)?;
}
match method {
"upper" => Ok(Value::String(s.to_uppercase().into())),
"lower" => Ok(Value::String(s.to_lowercase().into())),
"strip" => match strip_chars(method, args)? {
Some(chars) => Ok(Value::String(s.trim_matches(|c: char| chars.contains(c)).into())),
None => Ok(Value::String(s.trim().into())),
},
"lstrip" => match strip_chars(method, args)? {
Some(chars) => {
Ok(Value::String(s.trim_start_matches(|c: char| chars.contains(c)).into()))
}
None => Ok(Value::String(s.trim_start().into())),
},
"rstrip" => match strip_chars(method, args)? {
Some(chars) => {
Ok(Value::String(s.trim_end_matches(|c: char| chars.contains(c)).into()))
}
None => Ok(Value::String(s.trim_end().into())),
},
"split" => {
let bound = bind_method_params(method, args, kwargs, &["sep", "maxsplit"])?;
let maxsplit = coerce_maxsplit(bound[1].as_ref())?;
match &bound[0] {
None | Some(Value::None) => {
Ok(Value::List(shared_list(split_whitespace_max(s, maxsplit)?)))
}
Some(Value::String(sep)) => {
if sep.is_empty() {
return Err(InterpreterError::ValueError("empty separator".into()).into());
}
let parts: Vec<Value> = if maxsplit < 0 {
s.split(sep.as_str()).map(|p| Value::String(p.into())).collect()
} else {
let n = to_index(maxsplit + 1)?;
s.splitn(n, sep.as_str()).map(|p| Value::String(p.into())).collect()
};
Ok(Value::List(shared_list(parts)))
}
Some(other) => Err(InterpreterError::TypeError(format!(
"must be str or None, not {}",
other.type_name()
))
.into()),
}
}
"rsplit" => {
let bound = bind_method_params(method, args, kwargs, &["sep", "maxsplit"])?;
let maxsplit = coerce_maxsplit(bound[1].as_ref())?;
match &bound[0] {
None | Some(Value::None) => {
Ok(Value::List(shared_list(rsplit_whitespace_max(s, maxsplit)?)))
}
Some(Value::String(sep)) => {
if sep.is_empty() {
return Err(InterpreterError::ValueError("empty separator".into()).into());
}
let parts: Vec<Value> = if maxsplit < 0 {
s.split(sep.as_str()).map(|p| Value::String(p.into())).collect()
} else {
let n = to_index(maxsplit + 1)?;
let mut parts: Vec<Value> =
s.rsplitn(n, sep.as_str()).map(|p| Value::String(p.into())).collect();
parts.reverse();
parts
};
Ok(Value::List(shared_list(parts)))
}
Some(other) => Err(InterpreterError::TypeError(format!(
"must be str or None, not {}",
other.type_name()
))
.into()),
}
}
"join" => {
if args.len() != 1 {
return Err(InterpreterError::TypeError(
"join() takes exactly one argument".into(),
)
.into());
}
let items = iterate_value(&args[0]).map_err(|_| {
EvalError::from(InterpreterError::TypeError("can only join an iterable".into()))
})?;
let parts: Result<Vec<compact_str::CompactString>, _> = items
.into_iter()
.enumerate()
.map(|(i, v)| match v {
Value::String(s) => Ok(s),
_ => Err(EvalError::from(InterpreterError::TypeError(format!(
"sequence item {i}: expected str instance, {} found",
v.type_name()
)))),
})
.collect();
let owned = parts?;
let str_parts: Vec<&str> =
owned.iter().map(compact_str::CompactString::as_str).collect();
Ok(Value::String(str_parts.join(s).into()))
}
"replace" => {
if args.len() < 2 || args.len() > 3 {
return Err(
InterpreterError::TypeError("replace() takes 2 or 3 arguments".into()).into()
);
}
let old = match &args[0] {
Value::String(s) => s.as_str(),
_ => {
return Err(InterpreterError::TypeError(
"replace() argument must be str".into(),
)
.into());
}
};
let new = match &args[1] {
Value::String(s) => s.as_str(),
_ => {
return Err(InterpreterError::TypeError(
"replace() argument must be str".into(),
)
.into());
}
};
let count = if args.len() == 3 { value_to_i64(&args[2])? } else { -1 };
if count < 0 {
Ok(Value::String(s.replace(old, new).into()))
} else {
Ok(Value::String(s.replacen(old, new, to_index(count)?).into()))
}
}
"startswith" => string_affix(s, method, args, true),
"endswith" => string_affix(s, method, args, false),
"casefold" => Ok(Value::String(unicode_casefold(s).into())),
"encode" => {
let bound = bind_method_params(method, args, kwargs, &["encoding", "errors"])?;
let encoding = match &bound[0] {
Some(Value::String(name)) => name.as_str(),
None => "utf-8",
Some(_) => {
return Err(InterpreterError::TypeError(
"encode() argument must be str".into(),
)
.into());
}
};
let errors = match &bound[1] {
Some(Value::String(e)) => e.as_str(),
None => "strict",
Some(_) => {
return Err(InterpreterError::TypeError("errors must be str".into()).into());
}
};
match encoding.to_ascii_lowercase().as_str() {
"utf-8" | "utf_8" | "u8" => Ok(Value::Bytes(s.as_bytes().to_vec())),
"ascii" | "us-ascii" => Ok(Value::Bytes(encode_narrow(s, 0x7f, "ascii", errors)?)),
"latin-1" | "latin1" | "iso-8859-1" | "iso8859-1" => {
Ok(Value::Bytes(encode_narrow(s, 0xff, "latin-1", errors)?))
}
"utf-16" | "utf16" => {
let mut out = vec![0xFF, 0xFE];
for u in s.encode_utf16() {
out.extend_from_slice(&u.to_le_bytes());
}
Ok(Value::Bytes(out))
}
"utf-16-le" | "utf-16le" | "utf_16_le" => {
let mut out = Vec::with_capacity(s.len() * 2);
for u in s.encode_utf16() {
out.extend_from_slice(&u.to_le_bytes());
}
Ok(Value::Bytes(out))
}
"utf-16-be" | "utf-16be" | "utf_16_be" => {
let mut out = Vec::with_capacity(s.len() * 2);
for u in s.encode_utf16() {
out.extend_from_slice(&u.to_be_bytes());
}
Ok(Value::Bytes(out))
}
"utf-32" | "utf32" => {
let mut out = vec![0xFF, 0xFE, 0x00, 0x00];
for c in s.chars() {
out.extend_from_slice(&(c as u32).to_le_bytes());
}
Ok(Value::Bytes(out))
}
"utf-32-le" | "utf-32le" | "utf_32_le" => {
let mut out = Vec::with_capacity(s.len() * 4);
for c in s.chars() {
out.extend_from_slice(&(c as u32).to_le_bytes());
}
Ok(Value::Bytes(out))
}
"utf-32-be" | "utf-32be" | "utf_32_be" => {
let mut out = Vec::with_capacity(s.len() * 4);
for c in s.chars() {
out.extend_from_slice(&(c as u32).to_be_bytes());
}
Ok(Value::Bytes(out))
}
"unicode-escape" | "unicode_escape" => Ok(Value::Bytes(unicode_escape_encode(s))),
other => Err(EvalError::Exception(ExceptionValue::new(
"LookupError",
format!("unknown encoding: {other}"),
))),
}
}
"expandtabs" => {
let bound = bind_method_params(method, args, kwargs, &["tabsize"])?;
let tabsize = match &bound[0] {
Some(Value::Int(n)) => usize::try_from((*n).max(0)).unwrap_or(0),
Some(Value::Bool(b)) => usize::from(*b),
Some(_) => {
return Err(InterpreterError::TypeError(
"expandtabs() argument must be int".into(),
)
.into());
}
None => 8,
};
let mut out = String::with_capacity(s.len());
let mut col = 0usize;
for c in s.chars() {
match c {
'\t' => {
let pad = if tabsize == 0 { 0 } else { tabsize - col % tabsize };
for _ in 0..pad {
out.push(' ');
}
col += pad;
}
'\n' | '\r' => {
out.push(c);
col = 0;
}
_ => {
out.push(c);
col += 1;
}
}
}
Ok(Value::String(out.into()))
}
"partition" => {
let Value::String(sep) = args.first().ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(
"partition() requires 1 argument".into(),
))
})?
else {
return Err(
InterpreterError::TypeError("partition() argument must be str".into()).into()
);
};
Ok(Value::Tuple(s.find(sep.as_str()).map_or_else(
|| {
vec![
Value::String(s.into()),
Value::String("".into()),
Value::String("".into()),
]
},
|idx| {
vec![
Value::String(s[..idx].into()),
Value::String(sep.clone()),
Value::String(s[idx + sep.len()..].into()),
]
},
)))
}
"rpartition" => {
let Value::String(sep) = args.first().ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(
"rpartition() requires 1 argument".into(),
))
})?
else {
return Err(InterpreterError::TypeError(
"rpartition() argument must be str".into(),
)
.into());
};
Ok(Value::Tuple(s.rfind(sep.as_str()).map_or_else(
|| {
vec![
Value::String("".into()),
Value::String("".into()),
Value::String(s.into()),
]
},
|idx| {
vec![
Value::String(s[..idx].into()),
Value::String(sep.clone()),
Value::String(s[idx + sep.len()..].into()),
]
},
)))
}
"removeprefix" => {
if args.is_empty() {
return Err(InterpreterError::TypeError(
"removeprefix() takes exactly 1 argument".into(),
)
.into());
}
let Value::String(prefix) = &args[0] else {
return Err(InterpreterError::TypeError(
"removeprefix() argument must be str".into(),
)
.into());
};
Ok(Value::String(s.strip_prefix(prefix.as_str()).unwrap_or(s).into()))
}
"removesuffix" => {
if args.is_empty() {
return Err(InterpreterError::TypeError(
"removesuffix() takes exactly 1 argument".into(),
)
.into());
}
let Value::String(suffix) = &args[0] else {
return Err(InterpreterError::TypeError(
"removesuffix() argument must be str".into(),
)
.into());
};
Ok(Value::String(s.strip_suffix(suffix.as_str()).unwrap_or(s).into()))
}
"find" => {
let (sub, start, end) = parse_search_args(method, args)?;
let (start_char, bs, be) = resolve_window(s, start, end);
match s[bs..be].find(sub) {
Some(pos) => {
Ok(Value::Int(to_len_i64(start_char + s[bs..bs + pos].chars().count())?))
}
None => Ok(Value::Int(-1)),
}
}
"rfind" => {
let (sub, start, end) = parse_search_args(method, args)?;
let (start_char, bs, be) = resolve_window(s, start, end);
match s[bs..be].rfind(sub) {
Some(pos) => {
Ok(Value::Int(to_len_i64(start_char + s[bs..bs + pos].chars().count())?))
}
None => Ok(Value::Int(-1)),
}
}
"index" => {
let (sub, start, end) = parse_search_args(method, args)?;
let (start_char, bs, be) = resolve_window(s, start, end);
match s[bs..be].find(sub) {
Some(pos) => {
Ok(Value::Int(to_len_i64(start_char + s[bs..bs + pos].chars().count())?))
}
None => Err(EvalError::Exception(ExceptionValue::new(
"ValueError",
"substring not found",
))),
}
}
"rindex" => {
let (sub, start, end) = parse_search_args(method, args)?;
let (start_char, bs, be) = resolve_window(s, start, end);
match s[bs..be].rfind(sub) {
Some(pos) => {
Ok(Value::Int(to_len_i64(start_char + s[bs..bs + pos].chars().count())?))
}
None => Err(EvalError::Exception(ExceptionValue::new(
"ValueError",
"substring not found",
))),
}
}
"count" => {
let (sub, start, end) = parse_search_args(method, args)?;
let (_, bs, be) = resolve_window(s, start, end);
Ok(Value::Int(to_len_i64(s[bs..be].matches(sub).count())?))
}
"isdigit" => Ok(Value::Bool(!s.is_empty() && s.chars().all(is_unicode_digit))),
"isalpha" => Ok(Value::Bool(!s.is_empty() && s.chars().all(char::is_alphabetic))),
"isalnum" => Ok(Value::Bool(!s.is_empty() && s.chars().all(char::is_alphanumeric))),
"isspace" => Ok(Value::Bool(!s.is_empty() && s.chars().all(char::is_whitespace))),
"isupper" => {
Ok(Value::Bool(s.chars().any(char::is_uppercase) && !s.chars().any(char::is_lowercase)))
}
"islower" => {
Ok(Value::Bool(s.chars().any(char::is_lowercase) && !s.chars().any(char::is_uppercase)))
}
"title" => {
let mut result = String::new();
let mut capitalize_next = true;
for ch in s.chars() {
if !ch.is_alphabetic() {
result.push(ch);
capitalize_next = true;
} else if capitalize_next {
push_titlecase(&mut result, ch);
capitalize_next = false;
} else {
result.extend(ch.to_lowercase());
}
}
Ok(Value::String(result.into()))
}
"capitalize" => {
let mut chars = s.chars();
let result = chars.next().map_or_else(String::new, |first| {
let rest: String = chars.flat_map(char::to_lowercase).collect();
let mut head = String::new();
push_titlecase(&mut head, first);
format!("{head}{rest}")
});
Ok(Value::String(result.into()))
}
"swapcase" => {
let result: String = s
.chars()
.flat_map(|c| {
if c.is_uppercase() {
c.to_lowercase().collect::<Vec<_>>()
} else {
c.to_uppercase().collect::<Vec<_>>()
}
})
.collect();
Ok(Value::String(result.into()))
}
"center" => {
if args.is_empty() {
return Err(InterpreterError::TypeError(
"center() takes at least 1 argument".into(),
)
.into());
}
let width = to_index(value_to_i64(&args[0])?)?;
let fill = if args.len() >= 2 {
match &args[1] {
Value::String(f) => f.chars().next().unwrap_or(' '),
_ => ' ',
}
} else {
' '
};
let len = s.chars().count();
if len >= width {
Ok(Value::String(s.into()))
} else {
let total_pad = width - len;
let left_pad = total_pad / 2;
let right_pad = total_pad - left_pad;
let mut result = String::new();
for _ in 0..left_pad {
result.push(fill);
}
result.push_str(s);
for _ in 0..right_pad {
result.push(fill);
}
Ok(Value::String(result.into()))
}
}
"ljust" => {
if args.is_empty() {
return Err(InterpreterError::TypeError(
"ljust() takes at least 1 argument".into(),
)
.into());
}
let width = to_index(value_to_i64(&args[0])?)?;
let fill = if args.len() >= 2 {
match &args[1] {
Value::String(f) => f.chars().next().unwrap_or(' '),
_ => ' ',
}
} else {
' '
};
let len = s.chars().count();
if len >= width {
Ok(Value::String(s.into()))
} else {
let mut result = s.to_string();
for _ in 0..(width - len) {
result.push(fill);
}
Ok(Value::String(result.into()))
}
}
"rjust" => {
if args.is_empty() {
return Err(InterpreterError::TypeError(
"rjust() takes at least 1 argument".into(),
)
.into());
}
let width = to_index(value_to_i64(&args[0])?)?;
let fill = if args.len() >= 2 {
match &args[1] {
Value::String(f) => f.chars().next().unwrap_or(' '),
_ => ' ',
}
} else {
' '
};
let len = s.chars().count();
if len >= width {
Ok(Value::String(s.into()))
} else {
let mut result = String::new();
for _ in 0..(width - len) {
result.push(fill);
}
result.push_str(s);
Ok(Value::String(result.into()))
}
}
"zfill" => {
if args.is_empty() {
return Err(
InterpreterError::TypeError("zfill() takes exactly 1 argument".into()).into()
);
}
let width = to_index(value_to_i64(&args[0])?)?;
let len = s.chars().count();
if len >= width {
Ok(Value::String(s.into()))
} else {
let (sign, digits) = if s.starts_with('-') || s.starts_with('+') {
(&s[..1], &s[1..])
} else {
("", s)
};
let zeros = width - len;
let mut result = String::from(sign);
for _ in 0..zeros {
result.push('0');
}
result.push_str(digits);
Ok(Value::String(result.into()))
}
}
"splitlines" => {
let bound = bind_method_params(method, args, kwargs, &["keepends"])?;
let keepends = match &bound[0] {
None | Some(Value::None) => false,
Some(v) => v.is_truthy(),
};
Ok(Value::List(shared_list(split_lines(s, keepends))))
}
"isidentifier" => Ok(Value::Bool(is_identifier(s))),
"istitle" => Ok(Value::Bool(is_title(s))),
"isprintable" => {
Ok(Value::Bool(s.chars().all(crate::value::char_is_printable)))
}
"isascii" => Ok(Value::Bool(s.is_ascii())),
"isdecimal" => {
Ok(Value::Bool(
!s.is_empty()
&& s.chars().all(|c| get_general_category(c) == GeneralCategory::DecimalNumber),
))
}
"isnumeric" => {
Ok(Value::Bool(
!s.is_empty()
&& s.chars().all(|c| {
matches!(
get_general_category(c),
GeneralCategory::DecimalNumber
| GeneralCategory::LetterNumber
| GeneralCategory::OtherNumber
)
}),
))
}
"translate" => {
let Some(table) = args.first() else {
return Err(InterpreterError::TypeError(
"translate() takes exactly one argument (0 given)".into(),
)
.into());
};
translate(s, table)
}
"maketrans" => crate::eval::functions::helpers::str_maketrans(args),
_ => Err(InterpreterError::AttributeError(format!(
"'str' object has no attribute '{method}'"
))
.into()),
}
}
fn split_lines(s: &str, keepends: bool) -> Vec<Value> {
let mut out = Vec::new();
let mut line = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
let is_break = matches!(
c,
'\n' | '\r'
| '\u{0b}'
| '\u{0c}'
| '\u{1c}'
| '\u{1d}'
| '\u{1e}'
| '\u{85}'
| '\u{2028}'
| '\u{2029}'
);
if is_break {
if keepends {
line.push(c);
if c == '\r' && chars.peek() == Some(&'\n') {
line.push(chars.next().unwrap_or('\n'));
}
} else if c == '\r' && chars.peek() == Some(&'\n') {
chars.next();
}
out.push(Value::String(std::mem::take(&mut line).into()));
} else {
line.push(c);
}
}
if !line.is_empty() {
out.push(Value::String(line.into()));
}
out
}
fn is_identifier(s: &str) -> bool {
let mut chars = s.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first == '_' || first.is_alphabetic()) {
return false;
}
chars.all(|c| c == '_' || c.is_alphanumeric())
}
#[allow(
clippy::too_many_lines,
clippy::single_char_add_str,
reason = "the arms ARE the Unicode SpecialCasing titlecase table — kept uniform (all push_str) so it reads as one auditable data shape"
)]
fn push_titlecase(out: &mut String, c: char) {
match c {
'\u{01C4}' | '\u{01C5}' | '\u{01C6}' => out.push('\u{01C5}'),
'\u{01C7}' | '\u{01C8}' | '\u{01C9}' => out.push('\u{01C8}'),
'\u{01CA}' | '\u{01CB}' | '\u{01CC}' => out.push('\u{01CB}'),
'\u{01F1}' | '\u{01F2}' | '\u{01F3}' => out.push('\u{01F2}'),
'\u{10D0}'..='\u{10FF}' => out.push(c),
'\u{00DF}' => out.push_str("\u{0053}\u{0073}"),
'\u{0587}' => out.push_str("\u{0535}\u{0582}"),
'\u{1F80}' => out.push_str("\u{1F88}"),
'\u{1F81}' => out.push_str("\u{1F89}"),
'\u{1F82}' => out.push_str("\u{1F8A}"),
'\u{1F83}' => out.push_str("\u{1F8B}"),
'\u{1F84}' => out.push_str("\u{1F8C}"),
'\u{1F85}' => out.push_str("\u{1F8D}"),
'\u{1F86}' => out.push_str("\u{1F8E}"),
'\u{1F87}' => out.push_str("\u{1F8F}"),
'\u{1F88}' => out.push_str("\u{1F88}"),
'\u{1F89}' => out.push_str("\u{1F89}"),
'\u{1F8A}' => out.push_str("\u{1F8A}"),
'\u{1F8B}' => out.push_str("\u{1F8B}"),
'\u{1F8C}' => out.push_str("\u{1F8C}"),
'\u{1F8D}' => out.push_str("\u{1F8D}"),
'\u{1F8E}' => out.push_str("\u{1F8E}"),
'\u{1F8F}' => out.push_str("\u{1F8F}"),
'\u{1F90}' => out.push_str("\u{1F98}"),
'\u{1F91}' => out.push_str("\u{1F99}"),
'\u{1F92}' => out.push_str("\u{1F9A}"),
'\u{1F93}' => out.push_str("\u{1F9B}"),
'\u{1F94}' => out.push_str("\u{1F9C}"),
'\u{1F95}' => out.push_str("\u{1F9D}"),
'\u{1F96}' => out.push_str("\u{1F9E}"),
'\u{1F97}' => out.push_str("\u{1F9F}"),
'\u{1F98}' => out.push_str("\u{1F98}"),
'\u{1F99}' => out.push_str("\u{1F99}"),
'\u{1F9A}' => out.push_str("\u{1F9A}"),
'\u{1F9B}' => out.push_str("\u{1F9B}"),
'\u{1F9C}' => out.push_str("\u{1F9C}"),
'\u{1F9D}' => out.push_str("\u{1F9D}"),
'\u{1F9E}' => out.push_str("\u{1F9E}"),
'\u{1F9F}' => out.push_str("\u{1F9F}"),
'\u{1FA0}' => out.push_str("\u{1FA8}"),
'\u{1FA1}' => out.push_str("\u{1FA9}"),
'\u{1FA2}' => out.push_str("\u{1FAA}"),
'\u{1FA3}' => out.push_str("\u{1FAB}"),
'\u{1FA4}' => out.push_str("\u{1FAC}"),
'\u{1FA5}' => out.push_str("\u{1FAD}"),
'\u{1FA6}' => out.push_str("\u{1FAE}"),
'\u{1FA7}' => out.push_str("\u{1FAF}"),
'\u{1FA8}' => out.push_str("\u{1FA8}"),
'\u{1FA9}' => out.push_str("\u{1FA9}"),
'\u{1FAA}' => out.push_str("\u{1FAA}"),
'\u{1FAB}' => out.push_str("\u{1FAB}"),
'\u{1FAC}' => out.push_str("\u{1FAC}"),
'\u{1FAD}' => out.push_str("\u{1FAD}"),
'\u{1FAE}' => out.push_str("\u{1FAE}"),
'\u{1FAF}' => out.push_str("\u{1FAF}"),
'\u{1FB2}' => out.push_str("\u{1FBA}\u{0345}"),
'\u{1FB3}' => out.push_str("\u{1FBC}"),
'\u{1FB4}' => out.push_str("\u{0386}\u{0345}"),
'\u{1FB7}' => out.push_str("\u{0391}\u{0342}\u{0345}"),
'\u{1FBC}' => out.push_str("\u{1FBC}"),
'\u{1FC2}' => out.push_str("\u{1FCA}\u{0345}"),
'\u{1FC3}' => out.push_str("\u{1FCC}"),
'\u{1FC4}' => out.push_str("\u{0389}\u{0345}"),
'\u{1FC7}' => out.push_str("\u{0397}\u{0342}\u{0345}"),
'\u{1FCC}' => out.push_str("\u{1FCC}"),
'\u{1FF2}' => out.push_str("\u{1FFA}\u{0345}"),
'\u{1FF3}' => out.push_str("\u{1FFC}"),
'\u{1FF4}' => out.push_str("\u{038F}\u{0345}"),
'\u{1FF7}' => out.push_str("\u{03A9}\u{0342}\u{0345}"),
'\u{1FFC}' => out.push_str("\u{1FFC}"),
'\u{FB00}' => out.push_str("\u{0046}\u{0066}"),
'\u{FB01}' => out.push_str("\u{0046}\u{0069}"),
'\u{FB02}' => out.push_str("\u{0046}\u{006C}"),
'\u{FB03}' => out.push_str("\u{0046}\u{0066}\u{0069}"),
'\u{FB04}' => out.push_str("\u{0046}\u{0066}\u{006C}"),
'\u{FB05}' | '\u{FB06}' => out.push_str("\u{0053}\u{0074}"),
'\u{FB13}' => out.push_str("\u{0544}\u{0576}"),
'\u{FB14}' => out.push_str("\u{0544}\u{0565}"),
'\u{FB15}' => out.push_str("\u{0544}\u{056B}"),
'\u{FB16}' => out.push_str("\u{054E}\u{0576}"),
'\u{FB17}' => out.push_str("\u{0544}\u{056D}"),
_ => out.extend(c.to_uppercase()),
}
}
fn is_title(s: &str) -> bool {
let mut seen_cased = false;
let mut prev_cased = false;
for c in s.chars() {
if c.is_uppercase() {
if prev_cased {
return false;
}
seen_cased = true;
prev_cased = true;
} else if c.is_lowercase() {
if !prev_cased {
return false;
}
seen_cased = true;
prev_cased = true;
} else {
prev_cased = false;
}
}
seen_cased
}
fn translate(s: &str, table: &Value) -> EvalResult {
let Value::Dict(map) = table else {
return Err(InterpreterError::TypeError(format!(
"'{}' object is not subscriptable",
table.type_name()
))
.into());
};
let map = map.lock();
let mut out = String::with_capacity(s.len());
for c in s.chars() {
let key = crate::value::ValueKey::Int(i64::from(u32::from(c)));
match map.get(&key) {
None => out.push(c),
Some(Value::None) => {}
Some(Value::Int(n)) => {
let code = u32::try_from(*n).ok().and_then(char::from_u32).ok_or_else(|| {
EvalError::from(InterpreterError::ValueError(
"character mapping must be in range(0x110000)".into(),
))
})?;
out.push(code);
}
Some(Value::String(rep)) => out.push_str(rep),
Some(other) => {
return Err(InterpreterError::TypeError(format!(
"character mapping must return integer, None or str, not {}",
other.type_name()
))
.into());
}
}
}
Ok(Value::String(out.into()))
}
fn strip_chars<'a>(method: &str, args: &'a [Value]) -> Result<Option<&'a str>, EvalError> {
match args.first() {
None | Some(Value::None) => Ok(None),
Some(Value::String(chars)) => Ok(Some(chars.as_str())),
Some(_) => {
Err(InterpreterError::TypeError(format!("{method} arg must be None or str")).into())
}
}
}
fn coerce_maxsplit(arg: Option<&Value>) -> Result<i64, EvalError> {
match arg {
None | Some(Value::None) => Ok(-1),
Some(v) => value_to_i64(v),
}
}
fn parse_search_args<'a>(
method: &str,
args: &'a [Value],
) -> Result<(&'a str, Option<i64>, Option<i64>), EvalError> {
if args.is_empty() || args.len() > 3 {
return Err(
InterpreterError::TypeError(format!("{method}() takes at least 1 argument")).into()
);
}
let Value::String(sub) = &args[0] else {
return Err(InterpreterError::TypeError(format!("{method}() argument must be str")).into());
};
Ok((sub.as_str(), opt_index_arg(args.get(1))?, opt_index_arg(args.get(2))?))
}
fn resolve_window(s: &str, start: Option<i64>, end: Option<i64>) -> (usize, usize, usize) {
let char_len = s.chars().count() as i64;
let clamp = |i: i64| -> i64 {
let i = if i < 0 { i + char_len } else { i };
i.clamp(0, char_len)
};
let start = clamp(start.unwrap_or(0));
let end = clamp(end.unwrap_or(char_len)).max(start);
let (start, end) = (start as usize, end as usize);
(start, char_to_byte(s, start), char_to_byte(s, end))
}
fn char_to_byte(s: &str, char_idx: usize) -> usize {
s.char_indices().nth(char_idx).map_or(s.len(), |(b, _)| b)
}
fn string_affix(s: &str, method: &str, args: &[Value], is_start: bool) -> EvalResult {
if args.is_empty() || args.len() > 3 {
return Err(
InterpreterError::TypeError(format!("{method}() takes at least 1 argument")).into()
);
}
let (_, bs, be) = resolve_window(s, opt_index_arg(args.get(1))?, opt_index_arg(args.get(2))?);
let window = &s[bs..be];
let test = |affix: &str| {
if is_start { window.starts_with(affix) } else { window.ends_with(affix) }
};
let matched = match &args[0] {
Value::String(p) => test(p.as_str()),
Value::Tuple(items) => {
let mut any = false;
for it in items {
let Value::String(p) = it else {
return Err(InterpreterError::TypeError(format!(
"tuple for {method}() must only contain str"
))
.into());
};
if test(p.as_str()) {
any = true;
break;
}
}
any
}
_ => {
return Err(InterpreterError::TypeError(format!(
"{method}() first arg must be str or a tuple of str"
))
.into());
}
};
Ok(Value::Bool(matched))
}
fn split_whitespace_max(s: &str, maxsplit: i64) -> Result<Vec<Value>, EvalError> {
if maxsplit == 0 {
return Ok(vec![Value::String(s.into())]);
}
let mut parts = Vec::new();
let mut rest = s.trim_start();
let mut splits = 0i64;
while !rest.is_empty() {
if maxsplit >= 0 && splits >= maxsplit {
parts.push(Value::String(rest.into()));
break;
}
if let Some(ws) = rest.find(char::is_whitespace) {
parts.push(Value::String(rest[..ws].into()));
rest = rest[ws..].trim_start();
splits += 1;
} else {
parts.push(Value::String(rest.into()));
break;
}
}
if parts.is_empty() && s.chars().all(char::is_whitespace) {
return Ok(parts);
}
Ok(parts)
}
fn rsplit_whitespace_max(s: &str, maxsplit: i64) -> Result<Vec<Value>, EvalError> {
if maxsplit < 0 {
return Ok(s.split_whitespace().map(|p| Value::String(p.into())).collect());
}
if maxsplit == 0 {
return Ok(vec![Value::String(s.into())]);
}
let words: Vec<&str> = s.split_whitespace().collect();
if words.is_empty() {
return Ok(Vec::new());
}
let n = words.len();
let keep = usize::try_from(maxsplit).unwrap_or(n).min(n);
if keep >= n {
return Ok(words.into_iter().map(|p| Value::String(p.into())).collect());
}
let target_word = words[n - keep];
let mut search_from = 0usize;
for w in &words[..n - keep] {
let idx = s[search_from..].find(w).map(|i| search_from + i).unwrap_or(search_from);
search_from = idx + w.len();
}
let rem_end =
s[search_from..].find(target_word).map(|i| search_from + i).unwrap_or(search_from);
let remainder = s[..rem_end].trim_end();
let mut parts = Vec::with_capacity(keep + 1);
parts.push(Value::String(remainder.into()));
for w in &words[n - keep..] {
parts.push(Value::String((*w).into()));
}
Ok(parts)
}
fn unicode_casefold(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'ß' | 'ẞ' => out.push_str("ss"),
'ς' | 'Σ' => out.push('σ'),
c => out.extend(c.to_lowercase()),
}
}
out
}