feat: 添加字符串工具函数(split, trim, substring, replace, contains, upper, lower, starts_with, ends_with)及相关测试用例
This commit is contained in:
@@ -180,3 +180,30 @@ fn is_palindrome(s) {
|
||||
|
||||
print("is_palindrome('radar'):", is_palindrome("radar"))
|
||||
print("is_palindrome('hello'):", is_palindrome("hello"))
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// 7. 字符串工具(split / trim / substring / replace)
|
||||
// ============================================================================
|
||||
|
||||
print("=== string utils ===")
|
||||
|
||||
let csv = "Alice,Bob,Carol,Dave"
|
||||
let parts = split(csv, ",")
|
||||
print("split CSV:", parts)
|
||||
print("names count:", len(parts))
|
||||
|
||||
let padded = " hello "
|
||||
print("trim:", "'" + trim(padded) + "'")
|
||||
|
||||
print("substring('hello', 1, 3):", substring("hello", 1, 3))
|
||||
|
||||
let template = "Hello, NAME!"
|
||||
let greeting = replace(template, "NAME", "Aster")
|
||||
print("replace:", greeting)
|
||||
|
||||
print("contains('hello', 'ell'):", contains("hello", "ell"))
|
||||
print("upper:", upper("hello"))
|
||||
print("lower:", lower("HELLO"))
|
||||
print("starts_with('hello', 'he'):", starts_with("hello", "he"))
|
||||
print("ends_with('hello', 'lo'):", ends_with("hello", "lo"))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
mod core;
|
||||
mod io;
|
||||
mod os;
|
||||
mod string;
|
||||
|
||||
use crate::interpreter::{Env, Value};
|
||||
use std::cell::RefCell;
|
||||
@@ -34,4 +35,15 @@ pub fn register_all(env: &Rc<RefCell<Env>>) {
|
||||
e.define("typeof".into(), Value::NativeFunction(core::typeof_fn), true);
|
||||
e.define("push".into(), Value::NativeFunction(core::push), true);
|
||||
e.define("pop".into(), Value::NativeFunction(core::pop), true);
|
||||
|
||||
// string — split, trim, substring, replace, contains, upper, lower, starts_with, ends_with
|
||||
e.define("split".into(), Value::NativeFunction(string::split), true);
|
||||
e.define("trim".into(), Value::NativeFunction(string::trim), true);
|
||||
e.define("substring".into(), Value::NativeFunction(string::substring), true);
|
||||
e.define("replace".into(), Value::NativeFunction(string::replace), true);
|
||||
e.define("contains".into(), Value::NativeFunction(string::contains), true);
|
||||
e.define("upper".into(), Value::NativeFunction(string::upper), true);
|
||||
e.define("lower".into(), Value::NativeFunction(string::lower), true);
|
||||
e.define("starts_with".into(), Value::NativeFunction(string::starts_with), true);
|
||||
e.define("ends_with".into(), Value::NativeFunction(string::ends_with), true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//! 标准库:string(split, trim, substring, replace, contains, upper, lower, starts_with, ends_with)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::Value;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
fn require_string(val: &Value, arg_name: &str) -> Result<String, RuntimeError> {
|
||||
match val {
|
||||
Value::String(s) => Ok(s.clone()),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: format!("{} must be a string", arg_name),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// split(str, delim) → array of substrings
|
||||
// ============================================================================
|
||||
|
||||
pub fn split(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "split() expects 2 arguments (string, delimiter)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let delim = require_string(&args[1], "Second argument (delimiter)")?;
|
||||
|
||||
let parts: Vec<Value> = if delim.is_empty() {
|
||||
// Split by empty string → characters
|
||||
s.chars().map(|c| Value::String(c.to_string())).collect()
|
||||
} else {
|
||||
s.split(&delim).map(|p| Value::String(p.to_string())).collect()
|
||||
};
|
||||
|
||||
Ok(Value::Array(Rc::new(RefCell::new(parts))))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// trim(str) → string with leading/trailing whitespace removed
|
||||
// ============================================================================
|
||||
|
||||
pub fn trim(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "trim() expects 1 argument (string)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "Argument")?;
|
||||
Ok(Value::String(s.trim().to_string()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// substring(str, start, len?) → substring
|
||||
// ============================================================================
|
||||
|
||||
pub fn substring(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "substring() expects at least 2 arguments (string, start, len?)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let start = as_usize(&args[1], "Second argument (start)")?;
|
||||
let chars: Vec<char> = s.chars().collect();
|
||||
let start = start.min(chars.len());
|
||||
let end = if args.len() >= 3 {
|
||||
(start + as_usize(&args[2], "Third argument (length)")?).min(chars.len())
|
||||
} else {
|
||||
chars.len()
|
||||
};
|
||||
Ok(Value::String(chars[start..end].iter().collect()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// replace(str, old, new) → string with all occurrences replaced
|
||||
// ============================================================================
|
||||
|
||||
pub fn replace(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 3 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "replace() expects 3 arguments (string, old, new)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let old = require_string(&args[1], "Second argument (old)")?;
|
||||
let new = require_string(&args[2], "Third argument (new)")?;
|
||||
|
||||
if old.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "replace(): 'old' must not be empty".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Value::String(s.replace(&old, &new)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// contains(str, needle) → bool
|
||||
// ============================================================================
|
||||
|
||||
pub fn contains(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "contains() expects 2 arguments (string, needle)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let needle = require_string(&args[1], "Second argument (needle)")?;
|
||||
Ok(Value::Bool(s.contains(&needle)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// upper(str) → uppercase (ASCII)
|
||||
// ============================================================================
|
||||
|
||||
pub fn upper(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "upper() expects 1 argument (string)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "Argument")?;
|
||||
Ok(Value::String(s.to_ascii_uppercase()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// lower(str) → lowercase (ASCII)
|
||||
// ============================================================================
|
||||
|
||||
pub fn lower(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "lower() expects 1 argument (string)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "Argument")?;
|
||||
Ok(Value::String(s.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// starts_with(str, prefix) → bool
|
||||
// ============================================================================
|
||||
|
||||
pub fn starts_with(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "starts_with() expects 2 arguments (string, prefix)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let prefix = require_string(&args[1], "Second argument (prefix)")?;
|
||||
Ok(Value::Bool(s.starts_with(&prefix)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ends_with(str, suffix) → bool
|
||||
// ============================================================================
|
||||
|
||||
pub fn ends_with(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "ends_with() expects 2 arguments (string, suffix)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = require_string(&args[0], "First argument")?;
|
||||
let suffix = require_string(&args[1], "Second argument (suffix)")?;
|
||||
Ok(Value::Bool(s.ends_with(&suffix)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper
|
||||
// ============================================================================
|
||||
|
||||
fn as_usize(val: &Value, arg_name: &str) -> Result<usize, RuntimeError> {
|
||||
match val {
|
||||
Value::Number(n) => {
|
||||
if *n < 0.0 || n.fract() != 0.0 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: format!("{} must be a non-negative integer, got {}", arg_name, n),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
Ok(*n as usize)
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: format!("{} must be a number", arg_name),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1259,3 +1259,163 @@ fn error_forin_non_iterable() {
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for for-in on non-iterable");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// String builtins
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_split_by_comma() {
|
||||
match eval_expr("split(\"a,b,c\", \",\")") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 3);
|
||||
match &arr[0] { Value::String(s) => assert_eq!(s, "a"), v => panic!("{:?}", v) }
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_split_by_empty_delim_returns_chars() {
|
||||
match eval_expr("split(\"ab\", \"\")") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 2);
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_split_no_match_returns_whole_string() {
|
||||
match eval_expr("split(\"hello\", \",\")") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 1);
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_trim_spaces() {
|
||||
match eval_expr("trim(\" hello \")") {
|
||||
Value::String(s) => assert_eq!(s, "hello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_trim_no_whitespace() {
|
||||
match eval_expr("trim(\"hello\")") {
|
||||
Value::String(s) => assert_eq!(s, "hello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_substring_start() {
|
||||
match eval_expr("substring(\"hello\", 1)") {
|
||||
Value::String(s) => assert_eq!(s, "ello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_substring_start_and_length() {
|
||||
match eval_expr("substring(\"hello\", 1, 3)") {
|
||||
Value::String(s) => assert_eq!(s, "ell"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_substring_start_beyond_length() {
|
||||
match eval_expr("substring(\"hi\", 10)") {
|
||||
Value::String(s) => assert_eq!(s, ""),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_replace_single() {
|
||||
match eval_expr("replace(\"hello world\", \"world\", \"aster\")") {
|
||||
Value::String(s) => assert_eq!(s, "hello aster"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_replace_multiple() {
|
||||
match eval_expr("replace(\"a,a,a\", \"a\", \"b\")") {
|
||||
Value::String(s) => assert_eq!(s, "b,b,b"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_replace_no_match() {
|
||||
match eval_expr("replace(\"hello\", \"x\", \"y\")") {
|
||||
Value::String(s) => assert_eq!(s, "hello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_contains_true() {
|
||||
assert_bool(&eval_expr("contains(\"hello\", \"ell\")"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_contains_false() {
|
||||
assert_bool(&eval_expr("contains(\"hello\", \"xyz\")"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_upper() {
|
||||
match eval_expr("upper(\"hello\")") {
|
||||
Value::String(s) => assert_eq!(s, "HELLO"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_lower() {
|
||||
match eval_expr("lower(\"HELLO\")") {
|
||||
Value::String(s) => assert_eq!(s, "hello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_starts_with_true() {
|
||||
assert_bool(&eval_expr("starts_with(\"hello\", \"he\")"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_starts_with_false() {
|
||||
assert_bool(&eval_expr("starts_with(\"hello\", \"lo\")"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_ends_with_true() {
|
||||
assert_bool(&eval_expr("ends_with(\"hello\", \"lo\")"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_ends_with_false() {
|
||||
assert_bool(&eval_expr("ends_with(\"hello\", \"he\")"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_split_wrong_arg_count() {
|
||||
let result = std::panic::catch_unwind(|| { eval_expr("split(\"a\")"); });
|
||||
assert!(result.is_err(), "Expected error for split with 1 arg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_trim_non_string() {
|
||||
let result = std::panic::catch_unwind(|| { eval_expr("trim(42)"); });
|
||||
assert!(result.is_err(), "Expected error for trim(42)");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user