diff --git a/COMPAT.md b/COMPAT.md index 09c81a34d..12c34115c 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -93,8 +93,8 @@ This document describes the SQLite compatibility status of Limbo: | random() | Yes | | | randomblob(N) | No | | | replace(X,Y,Z) | No | | -| round(X) | No | | -| round(X,Y) | No | | +| round(X) | Yes | | +| round(X,Y) | Yes | | | rtrim(X) | No | | | rtrim(X,Y) | No | | | sign(X) | No | | diff --git a/core/expr.rs b/core/expr.rs index 533a1207c..858e059af 100644 --- a/core/expr.rs +++ b/core/expr.rs @@ -405,6 +405,43 @@ pub fn translate_expr( } Ok(target_register) } + SingleRowFunc::Round => { + let args = if let Some(args) = args { + if args.len() > 2 { + anyhow::bail!( + "Parse error: round function with more than 2 arguments" + ); + } + args + } else { + anyhow::bail!("Parse error: round 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::Round, + }); + } 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::Round, + }); + } + Ok(target_register) + } } } None => { diff --git a/core/function.rs b/core/function.rs index b9189a8d4..0c3e4b271 100644 --- a/core/function.rs +++ b/core/function.rs @@ -36,6 +36,7 @@ pub enum SingleRowFunc { Lower, Random, Trim, + Round, } impl ToString for SingleRowFunc { @@ -48,6 +49,7 @@ impl ToString for SingleRowFunc { SingleRowFunc::Lower => "lower".to_string(), SingleRowFunc::Random => "random".to_string(), SingleRowFunc::Trim => "trim".to_string(), + SingleRowFunc::Round => "round".to_string(), } } } @@ -77,6 +79,7 @@ impl FromStr for Func { "lower" => Ok(Func::SingleRow(SingleRowFunc::Lower)), "random" => Ok(Func::SingleRow(SingleRowFunc::Random)), "trim" => Ok(Func::SingleRow(SingleRowFunc::Trim)), + "round" => Ok(Func::SingleRow(SingleRowFunc::Round)), _ => Err(()), } } diff --git a/core/vdbe.rs b/core/vdbe.rs index cd3fe137a..383f5cdec 100644 --- a/core/vdbe.rs +++ b/core/vdbe.rs @@ -1303,6 +1303,14 @@ impl Program { state.registers[*dest] = result; state.pc += 1; } + SingleRowFunc::Round => { + let start_reg = *start_reg; + let reg_value = state.registers[start_reg].clone(); + let precision_value = state.registers.get(start_reg + 1).cloned(); + let result = exec_round(®_value, precision_value); + state.registers[*dest] = result; + state.pc += 1; + } }, } } @@ -1888,6 +1896,27 @@ fn exec_like(pattern: &str, text: &str) -> bool { re.is_match(text) } +fn exec_round(reg: &OwnedValue, precision: Option) -> OwnedValue { + let precision = match precision { + Some(OwnedValue::Text(x)) => x.parse().unwrap_or(0.0), + Some(OwnedValue::Integer(x)) => x as f64, + Some(OwnedValue::Float(x)) => x, + None => 0.0, + _ => return OwnedValue::Null, + }; + + let reg = match reg { + OwnedValue::Text(x) => x.parse().unwrap_or(0.0), + OwnedValue::Integer(x) => *x as f64, + OwnedValue::Float(x) => *x, + _ => return reg.to_owned(), + }; + + let precision = if precision < 1.0 { 0.0 } else { precision }; + let multiplier = 10f64.powi(precision as i32); + OwnedValue::Float(((reg * multiplier).round()) / multiplier) +} + // Implements TRIM pattern matching. fn exec_trim(reg: &OwnedValue, pattern: Option) -> OwnedValue { match (reg, pattern) { @@ -1922,7 +1951,8 @@ 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_trim, exec_upper, OwnedValue, + exec_abs, exec_if, exec_like, exec_lower, exec_random, exec_round, exec_trim, exec_upper, + OwnedValue, }; use std::rc::Rc; @@ -2001,6 +2031,33 @@ mod tests { } } + #[test] + fn test_exec_round() { + let input_val = OwnedValue::Float(123.456); + let expected_val = OwnedValue::Float(123.0); + assert_eq!(exec_round(&input_val, None), expected_val); + + let input_val = OwnedValue::Float(123.456); + let precision_val = OwnedValue::Integer(2); + let expected_val = OwnedValue::Float(123.46); + assert_eq!(exec_round(&input_val, Some(precision_val)), expected_val); + + let input_val = OwnedValue::Float(123.456); + let precision_val = OwnedValue::Text(Rc::new(String::from("1"))); + let expected_val = OwnedValue::Float(123.5); + assert_eq!(exec_round(&input_val, Some(precision_val)), expected_val); + + let input_val = OwnedValue::Text(Rc::new(String::from("123.456"))); + let precision_val = OwnedValue::Integer(2); + let expected_val = OwnedValue::Float(123.46); + assert_eq!(exec_round(&input_val, Some(precision_val)), expected_val); + + let input_val = OwnedValue::Integer(123); + let precision_val = OwnedValue::Integer(1); + let expected_val = OwnedValue::Float(123.0); + assert_eq!(exec_round(&input_val, Some(precision_val)), expected_val); + } + #[test] fn test_exec_if() { let reg = OwnedValue::Integer(0); diff --git a/sqlite3/tests/Makefile b/sqlite3/tests/Makefile index 1450fb499..bec28696a 100644 --- a/sqlite3/tests/Makefile +++ b/sqlite3/tests/Makefile @@ -13,6 +13,7 @@ PROGRAM = sqlite3-tests CFLAGS = -g -Wall -std=c17 -MMD -MP LIBS ?= -lsqlite3 +LIBS += -lm OBJS += main.o OBJS += test-aux.o diff --git a/testing/scalar-functions.test b/testing/scalar-functions.test index 1f8dee6cc..5cdb864b6 100644 --- a/testing/scalar-functions.test +++ b/testing/scalar-functions.test @@ -85,4 +85,40 @@ do_execsql_test trim-pattern-null { do_execsql_test trim-no-match-pattern { SELECT trim('Limbo', 'xyz'); -} {Limbo} \ No newline at end of file +} {Limbo} + +do_execsql_test round-float-no-precision { + SELECT round(123.456); +} {123.0} + +do_execsql_test round-float-with-precision { + SELECT round(123.456, 2); +} {123.46} + +do_execsql_test round-float-with-text-precision { + SELECT round(123.5, '1'); +} {123.5} + +do_execsql_test round-text-parsable { + SELECT round('123.456', 2); +} {123.46} + +do_execsql_test round-text-non-parsable { + SELECT round('abc', 1); +} {0.0} + +do_execsql_test round-integer-with-precision { + SELECT round(123, 1); +} {123.0} + +do_execsql_test round-float-negative-precision { + SELECT round(123.456, -1); +} {123.0} + +do_execsql_test round-float-zero-precision { + SELECT round(123.456, 0); +} {123.0} + +do_execsql_test round-null-precision { + SELECT round(123.456, null); +} {}