diff --git a/COMPAT.md b/COMPAT.md index 7eb47d080..09c81a34d 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -109,8 +109,8 @@ This document describes the SQLite compatibility status of Limbo: | substring(X,Y,Z) | No | | | substring(X,Y) | No | | | total_changes() | No | | -| trim(X) | No | | -| trim(X,Y) | No | | +| trim(X) | Yes | | +| trim(X,Y) | Yes | | | typeof(X) | No | | | unhex(X) | No | | | unhex(X,Y) | No | | diff --git a/core/expr.rs b/core/expr.rs index 00a562034..533a1207c 100644 --- a/core/expr.rs +++ b/core/expr.rs @@ -366,7 +366,43 @@ pub fn translate_expr( dest: target_register, func: SingleRowFunc::Random, }); + Ok(target_register) + } + SingleRowFunc::Trim => { + let args = if let Some(args) = args { + if args.len() > 2 { + anyhow::bail!( + "Parse error: trim function with more than 2 arguments" + ); + } + args + } else { + anyhow::bail!("Parse error: trim function with no arguments"); + }; + if args.len() == 1 { + let regs = program.alloc_register(); + let _ = translate_expr(program, select, &args[0], regs)?; + program.emit_insn(Insn::Function { + start_reg: regs, + dest: target_register, + func: SingleRowFunc::Trim, + }); + } else { + for arg in args { + let reg = program.alloc_register(); + let _ = translate_expr(program, select, arg, reg)?; + match arg { + ast::Expr::Literal(_) => program.mark_last_insn_constant(), + _ => {} + } + } + program.emit_insn(Insn::Function { + start_reg: target_register + 1, + dest: target_register, + func: SingleRowFunc::Trim, + }); + } Ok(target_register) } } diff --git a/core/function.rs b/core/function.rs index b01369f2e..b9189a8d4 100644 --- a/core/function.rs +++ b/core/function.rs @@ -35,6 +35,7 @@ pub enum SingleRowFunc { Upper, Lower, Random, + Trim, } impl ToString for SingleRowFunc { @@ -46,6 +47,7 @@ impl ToString for SingleRowFunc { SingleRowFunc::Upper => "upper".to_string(), SingleRowFunc::Lower => "lower".to_string(), SingleRowFunc::Random => "random".to_string(), + SingleRowFunc::Trim => "trim".to_string(), } } } @@ -74,6 +76,7 @@ impl FromStr for Func { "upper" => Ok(Func::SingleRow(SingleRowFunc::Upper)), "lower" => Ok(Func::SingleRow(SingleRowFunc::Lower)), "random" => Ok(Func::SingleRow(SingleRowFunc::Random)), + "trim" => Ok(Func::SingleRow(SingleRowFunc::Trim)), _ => Err(()), } } diff --git a/core/vdbe.rs b/core/vdbe.rs index 1e82e7242..cd3fe137a 100644 --- a/core/vdbe.rs +++ b/core/vdbe.rs @@ -1293,6 +1293,16 @@ impl Program { state.registers[*dest] = exec_random(); state.pc += 1; } + SingleRowFunc::Trim => { + let start_reg = *start_reg; + let reg_value = state.registers[start_reg].clone(); + let pattern_value = state.registers.get(start_reg + 1).cloned(); + + let result = exec_trim(®_value, pattern_value); + + state.registers[*dest] = result; + state.pc += 1; + } }, } } @@ -1878,6 +1888,23 @@ fn exec_like(pattern: &str, text: &str) -> bool { re.is_match(text) } +// Implements TRIM pattern matching. +fn exec_trim(reg: &OwnedValue, pattern: Option) -> OwnedValue { + match (reg, pattern) { + (reg, Some(pattern)) => match reg { + OwnedValue::Text(_) | OwnedValue::Integer(_) | OwnedValue::Float(_) => { + let pattern_chars: Vec = pattern.to_string().chars().collect(); + OwnedValue::Text(Rc::new( + reg.to_string().trim_matches(&pattern_chars[..]).to_string(), + )) + } + _ => reg.to_owned(), + }, + (OwnedValue::Text(t), None) => OwnedValue::Text(Rc::new(t.trim().to_string())), + (reg, _) => reg.to_owned(), + } +} + // exec_if returns whether you should jump fn exec_if(reg: &OwnedValue, null_reg: &OwnedValue, not: bool) -> bool { match reg { @@ -1894,9 +1921,23 @@ fn exec_if(reg: &OwnedValue, null_reg: &OwnedValue, not: bool) -> bool { #[cfg(test)] mod tests { - use super::{exec_abs, exec_if, exec_like, exec_lower, exec_random, exec_upper, OwnedValue}; + use super::{ + exec_abs, exec_if, exec_like, exec_lower, exec_random, exec_trim, exec_upper, OwnedValue, + }; use std::rc::Rc; + #[test] + fn test_trim() { + let input_str = OwnedValue::Text(Rc::new(String::from(" Bob and Alice "))); + let expected_str = OwnedValue::Text(Rc::new(String::from("Bob and Alice"))); + assert_eq!(exec_trim(&input_str, None), expected_str); + + let input_str = OwnedValue::Text(Rc::new(String::from(" Bob and Alice "))); + let pattern_str = OwnedValue::Text(Rc::new(String::from("Bob and"))); + let expected_str = OwnedValue::Text(Rc::new(String::from("Alice"))); + assert_eq!(exec_trim(&input_str, Some(pattern_str)), expected_str); + } + #[test] fn test_upper_case() { let input_str = OwnedValue::Text(Rc::new(String::from("Limbo"))); diff --git a/testing/scalar-functions.test b/testing/scalar-functions.test index b93f82cd6..1f8dee6cc 100644 --- a/testing/scalar-functions.test +++ b/testing/scalar-functions.test @@ -50,3 +50,39 @@ do_execsql_test lower-char { do_execsql_test lower-null { select lower(null) } {} + +do_execsql_test trim { + SELECT trim(' Limbo '); +} {Limbo} + +do_execsql_test trim-number { + SELECT trim(1); +} {1} + +do_execsql_test trim-null { + SELECT trim(null); +} {} + +do_execsql_test trim-leading-whitespace { + SELECT trim(' Leading'); +} {Leading} + +do_execsql_test trim-trailing-whitespace { + SELECT trim('Trailing '); +} {Trailing} + +do_execsql_test trim-pattern { + SELECT trim('Limbo', 'Limbo'); +} {} + +do_execsql_test trim-pattern-number { + SELECT trim(1, '1'); +} {} + +do_execsql_test trim-pattern-null { + SELECT trim(null, 'null'); +} {} + +do_execsql_test trim-no-match-pattern { + SELECT trim('Limbo', 'xyz'); +} {Limbo} \ No newline at end of file