From d7d7e5e1e723c26293596c858b214089a247fe7b Mon Sep 17 00:00:00 2001 From: may Date: Fri, 5 Sep 2025 12:19:53 +0200 Subject: [PATCH 1/8] make round be round-ties-even --- crates/steel-core/src/primitives/numbers.rs | 64 ++++++++++++++++++--- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/crates/steel-core/src/primitives/numbers.rs b/crates/steel-core/src/primitives/numbers.rs index 2487dbd08..da5ff17c4 100644 --- a/crates/steel-core/src/primitives/numbers.rs +++ b/crates/steel-core/src/primitives/numbers.rs @@ -2,8 +2,9 @@ use crate::rvals::{IntoSteelVal, Result, SteelComplex, SteelVal}; use crate::{steelerr, stop, throw}; use num_bigint::BigInt; use num_integer::Integer; -use num_rational::{BigRational, Rational32}; -use num_traits::{pow::Pow, CheckedAdd, CheckedMul, Signed, ToPrimitive, Zero}; +use num_rational::{BigRational, Ratio, Rational32}; +use num_traits::{pow::Pow, CheckedAdd, CheckedMul, One, Signed, ToPrimitive, Zero}; +use std::cmp::Ordering; use std::ops::Neg; /// Checks if the given value is a number @@ -1112,7 +1113,54 @@ fn numerator(number: &SteelVal) -> Result { } } -/// Rounds the given number to the nearest integer. +/// Rounds to the nearest integer. Rounds half-way cases to even. +/// +/// Reimplementation of https://github.com/rust-num/num-rational/pull/141, +/// while that one isn't merged yet. +fn rational_round_ties_even(num: &Ratio) -> Ratio +where + T: Zero + One + Integer + Clone, +{ + let zero: Ratio = Zero::zero(); + let one: T = One::one(); + let two: T = one.clone() + one.clone(); + + // Find unsigned fractional part of rational number + let mut fractional = num.fract(); + if fractional < zero { + fractional = zero - fractional + }; + + // Compare the unsigned fractional part with 1/2 + let half = Ratio::new_raw(one, two); + match fractional.cmp(&half) { + Ordering::Greater => { + let one: Ratio = One::one(); + if *num >= Zero::zero() { + num.trunc() + one + } else { + num.trunc() - one + } + } + Ordering::Equal => { + let trunc = num.trunc(); + if trunc.numer().is_even() { + trunc + } else { + let one: Ratio = One::one(); + if *num >= Zero::zero() { + num.trunc() + one + } else { + num.trunc() - one + } + } + } + Ordering::Less => num.trunc(), + } +} + +/// Rounds the given number to the nearest integer, rounding half-way cases to +/// the even number. /// /// (round number) -> number? /// @@ -1122,15 +1170,17 @@ fn numerator(number: &SteelVal) -> Result { /// ```scheme /// > (round 3.14) ;; => 3 /// > (round 4.6) ;; => 5 -/// > (round -2.5) ;; => -3 +/// > (round 2.5) ;; => 2 +/// > (round 3.5) ;; => 4 +/// > (round -2.5) ;; => -2 /// ``` #[steel_derive::function(name = "round", constant = true)] fn round(number: &SteelVal) -> Result { match number { SteelVal::IntV(i) => i.into_steelval(), - SteelVal::NumV(n) => n.round().into_steelval(), - SteelVal::Rational(f) => f.round().into_steelval(), - SteelVal::BigRational(f) => f.round().into_steelval(), + SteelVal::NumV(n) => n.round_ties_even().into_steelval(), + SteelVal::Rational(f) => rational_round_ties_even(f).into_steelval(), + SteelVal::BigRational(f) => rational_round_ties_even(f).into_steelval(), SteelVal::BigNum(n) => Ok(SteelVal::BigNum(n.clone())), _ => steelerr!(TypeMismatch => "round expects a real number, found: {}", number), } From 63add6e6a5cd6024d9fe76879ae5c14bd09e3cad Mon Sep 17 00:00:00 2001 From: may Date: Fri, 5 Sep 2025 12:29:46 +0200 Subject: [PATCH 2/8] add some round-ties-even tests --- crates/steel-core/src/tests/success/numbers.scm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/steel-core/src/tests/success/numbers.scm b/crates/steel-core/src/tests/success/numbers.scm index e299f7eff..b46236d3b 100644 --- a/crates/steel-core/src/tests/success/numbers.scm +++ b/crates/steel-core/src/tests/success/numbers.scm @@ -220,6 +220,14 @@ (assert-equal! 3.0 (round 2.6)) (assert-equal! 9223372036854775808 (round 9223372036854775808)) +;; round-ties-even +(assert-equal! -2.0 (round -1.5)) +(assert-equal! 2.0 (round 2.5)) +(assert-equal! 4.0 (round 3.5)) +(assert-equal! 2 (round 5/2)) +(assert-equal! 4 (round (+ 4 1/2))) +(assert-equal! 4 (round 7/2)) + (assert-equal! 4 (square 2)) (assert-equal! 2 (sqrt 4)) (assert-equal! 4.0 (square 2.0)) From 8810fee84fcfa63938fa8b2c2dfccdd26a92bcfe Mon Sep 17 00:00:00 2001 From: may Date: Mon, 22 Sep 2025 16:19:51 +0200 Subject: [PATCH 3/8] allow using (truncate) with rationals --- crates/steel-core/src/primitives/numbers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/steel-core/src/primitives/numbers.rs b/crates/steel-core/src/primitives/numbers.rs index da5ff17c4..280fb2ae4 100644 --- a/crates/steel-core/src/primitives/numbers.rs +++ b/crates/steel-core/src/primitives/numbers.rs @@ -364,9 +364,9 @@ pub fn truncate(arg: &SteelVal) -> Result { match arg { SteelVal::NumV(n) => n.trunc().into_steelval(), SteelVal::IntV(i) => Ok(SteelVal::IntV(*i)), - // SteelVal::Rational(ratio) => ratio.trunc(), + SteelVal::Rational(ratio) => ratio.trunc().into_steelval(), SteelVal::BigNum(gc) => Ok(SteelVal::BigNum(gc.clone())), - // SteelVal::BigRational(gc) => gc.trunc(), + SteelVal::BigRational(gc) => gc.trunc().into_steelval(), _ => stop!(TypeMismatch => "truncate expects a real number, found: {}", arg), } } From 1cb1bc9fd4ab7e4bbd9226d2e2ad26827b948dde Mon Sep 17 00:00:00 2001 From: may Date: Mon, 22 Sep 2025 16:37:35 +0200 Subject: [PATCH 4/8] add tests for (truncate) --- crates/steel-core/src/tests/success/numbers.scm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/steel-core/src/tests/success/numbers.scm b/crates/steel-core/src/tests/success/numbers.scm index b46236d3b..008a6473e 100644 --- a/crates/steel-core/src/tests/success/numbers.scm +++ b/crates/steel-core/src/tests/success/numbers.scm @@ -188,16 +188,22 @@ (assert-equal! 10.0 (ceiling 9.1)) (assert-equal! 9.0 (floor 9.1)) +(assert-equal! 9.0 (truncate 9.1)) (assert-equal! 10.0 (ceiling 10.0)) (assert-equal! 10.0 (floor 10.0)) +(assert-equal! 10.0 (truncate 10.0)) (assert-equal! 10 (ceiling 10)) (assert-equal! 10 (floor 10)) +(assert-equal! 10 (truncate 10)) (assert-equal! -9.0 (ceiling -9.1)) (assert-equal! -10.0 (floor -9.1)) +(assert-equal! -9.0 (truncate -9.1)) (assert-equal! 1 (ceiling 1/2)) (assert-equal! 0 (floor 1/2)) +(assert-equal! 0 (truncate 1/2)) (assert-equal! 0 (ceiling -1/2)) (assert-equal! -1 (floor -1/2)) +(assert-equal! 0 (truncate -1/2)) (assert-equal! 3 (numerator 3)) (assert-equal! 3 (numerator 3/2)) From 79c6d6c32f2488c6f3ebf598fa1681d74e10df3e Mon Sep 17 00:00:00 2001 From: may Date: Mon, 22 Sep 2025 16:24:39 +0200 Subject: [PATCH 5/8] group all the rounding procedures together --- crates/steel-core/src/primitives/numbers.rs | 96 ++++++++++----------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/crates/steel-core/src/primitives/numbers.rs b/crates/steel-core/src/primitives/numbers.rs index 280fb2ae4..05b641521 100644 --- a/crates/steel-core/src/primitives/numbers.rs +++ b/crates/steel-core/src/primitives/numbers.rs @@ -359,18 +359,6 @@ pub fn multiply_primitive(args: &[SteelVal]) -> Result { multiply_primitive_impl(args) } -#[steel_derive::function(name = "truncate", constant = true)] -pub fn truncate(arg: &SteelVal) -> Result { - match arg { - SteelVal::NumV(n) => n.trunc().into_steelval(), - SteelVal::IntV(i) => Ok(SteelVal::IntV(*i)), - SteelVal::Rational(ratio) => ratio.trunc().into_steelval(), - SteelVal::BigNum(gc) => Ok(SteelVal::BigNum(gc.clone())), - SteelVal::BigRational(gc) => gc.trunc().into_steelval(), - _ => stop!(TypeMismatch => "truncate expects a real number, found: {}", arg), - } -} - /// Returns quotient of dividing numerator by denomintator. /// /// (quotient numerator denominator) -> integer? @@ -838,29 +826,6 @@ fn abs(number: &SteelVal) -> Result { } } -/// Rounds the given number up to the nearest integer not less than it. -/// -/// (ceiling number) -> integer? -/// -/// * number : number? - The number to round up. -/// -/// # Examples -/// ```scheme -/// > (ceiling 42) ;; => 42 -/// > (ceiling 42.1) ;; => 43 -/// > (ceiling -42.1) ;; => -42 -/// ``` -#[steel_derive::function(name = "ceiling", constant = true)] -fn ceiling(number: &SteelVal) -> Result { - match number { - n @ SteelVal::IntV(_) | n @ SteelVal::BigNum(_) => Ok(n.clone()), - SteelVal::NumV(n) => Ok(SteelVal::NumV(n.ceil())), - SteelVal::Rational(f) => f.ceil().into_steelval(), - SteelVal::BigRational(f) => f.ceil().into_steelval(), - _ => steelerr!(TypeMismatch => "ceiling expects a real number, found: {}", number), - } -} - /// Retrieves the denominator of the given rational number. /// /// (denominator number) -> integer? @@ -1066,6 +1031,29 @@ fn exp(left: &SteelVal) -> Result { } } +/// Retrieves the numerator of the given rational number. +/// +/// (numerator number) -> number? +/// +/// * number : number? - The rational number to retrieve the numerator from. +/// +/// # Examples +/// ```scheme +/// > (numerator 3/4) ;; => 3 +/// > (numerator 5/2) ;; => 5 +/// > (numerator -2) ;; => -2 +/// ``` +#[steel_derive::function(name = "numerator", constant = true)] +fn numerator(number: &SteelVal) -> Result { + match number { + SteelVal::IntV(x) => x.into_steelval(), + SteelVal::Rational(x) => (*x.numer() as isize).into_steelval(), + SteelVal::BigNum(x) => Ok(SteelVal::BigNum(x.clone())), + SteelVal::BigRational(x) => (x.numer().clone()).into_steelval(), + _ => steelerr!(Generic => "numerator expects an integer or rational number"), + } +} + /// Computes the largest integer less than or equal to the given number. /// /// (floor number) -> number? @@ -1090,26 +1078,38 @@ fn floor(number: &SteelVal) -> Result { } } -/// Retrieves the numerator of the given rational number. +/// Rounds the given number up to the nearest integer not less than it. /// -/// (numerator number) -> number? +/// (ceiling number) -> integer? /// -/// * number : number? - The rational number to retrieve the numerator from. +/// * number : number? - The number to round up. /// /// # Examples /// ```scheme -/// > (numerator 3/4) ;; => 3 -/// > (numerator 5/2) ;; => 5 -/// > (numerator -2) ;; => -2 +/// > (ceiling 42) ;; => 42 +/// > (ceiling 42.1) ;; => 43 +/// > (ceiling -42.1) ;; => -42 /// ``` -#[steel_derive::function(name = "numerator", constant = true)] -fn numerator(number: &SteelVal) -> Result { +#[steel_derive::function(name = "ceiling", constant = true)] +fn ceiling(number: &SteelVal) -> Result { match number { - SteelVal::IntV(x) => x.into_steelval(), - SteelVal::Rational(x) => (*x.numer() as isize).into_steelval(), - SteelVal::BigNum(x) => Ok(SteelVal::BigNum(x.clone())), - SteelVal::BigRational(x) => (x.numer().clone()).into_steelval(), - _ => steelerr!(Generic => "numerator expects an integer or rational number"), + n @ SteelVal::IntV(_) | n @ SteelVal::BigNum(_) => Ok(n.clone()), + SteelVal::NumV(n) => Ok(SteelVal::NumV(n.ceil())), + SteelVal::Rational(f) => f.ceil().into_steelval(), + SteelVal::BigRational(f) => f.ceil().into_steelval(), + _ => steelerr!(TypeMismatch => "ceiling expects a real number, found: {}", number), + } +} + +#[steel_derive::function(name = "truncate", constant = true)] +pub fn truncate(arg: &SteelVal) -> Result { + match arg { + SteelVal::NumV(n) => n.trunc().into_steelval(), + SteelVal::IntV(i) => Ok(SteelVal::IntV(*i)), + SteelVal::Rational(ratio) => ratio.trunc().into_steelval(), + SteelVal::BigNum(gc) => Ok(SteelVal::BigNum(gc.clone())), + SteelVal::BigRational(gc) => gc.trunc().into_steelval(), + _ => stop!(TypeMismatch => "truncate expects a real number, found: {}", arg), } } From 69a1c07d1fd58d8aa5c4dfdadcb61cd5bd572858 Mon Sep 17 00:00:00 2001 From: may Date: Mon, 22 Sep 2025 16:29:50 +0200 Subject: [PATCH 6/8] add docs for (truncate), reword docs for (floor) previously the (floor) and (ceiling) docs were worded very differently, so align them to better match eachother. additionally reword the error message for (floor) to also align them with the rest of the rounding procedures. --- crates/steel-core/src/primitives/numbers.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/steel-core/src/primitives/numbers.rs b/crates/steel-core/src/primitives/numbers.rs index 05b641521..f8bd74dce 100644 --- a/crates/steel-core/src/primitives/numbers.rs +++ b/crates/steel-core/src/primitives/numbers.rs @@ -1054,7 +1054,7 @@ fn numerator(number: &SteelVal) -> Result { } } -/// Computes the largest integer less than or equal to the given number. +/// Rounds the given number down to the nearest integer not larger than it. /// /// (floor number) -> number? /// @@ -1074,7 +1074,7 @@ fn floor(number: &SteelVal) -> Result { SteelVal::Rational(x) => x.floor().into_steelval(), SteelVal::BigNum(x) => Ok(SteelVal::BigNum(x.clone())), SteelVal::BigRational(x) => x.floor().into_steelval(), - _ => steelerr!(Generic => "floor expected a real number"), + _ => steelerr!(Generic => "floor expects a real number, found: {}", number), } } @@ -1101,6 +1101,20 @@ fn ceiling(number: &SteelVal) -> Result { } } +/// Rounds the given number to the nearest integer, whose absolute value is not +/// larger than it. +/// +/// (truncate number) -> integer? +/// +/// * number : number? - The number to truncate. +/// +/// # Examples +/// +/// ```scheme +/// > (truncate 42) ;; => 42 +/// > (truncate 42.1) ;; => 42 +/// > (truncate -42.1) ;; => -42 +/// ``` #[steel_derive::function(name = "truncate", constant = true)] pub fn truncate(arg: &SteelVal) -> Result { match arg { From c06f599615f6bbba97d56f8a3d704a6c47927ced Mon Sep 17 00:00:00 2001 From: may Date: Mon, 22 Sep 2025 16:33:07 +0200 Subject: [PATCH 7/8] document, that only real? numbers can be rounded --- crates/steel-core/src/primitives/numbers.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/steel-core/src/primitives/numbers.rs b/crates/steel-core/src/primitives/numbers.rs index f8bd74dce..ace37632c 100644 --- a/crates/steel-core/src/primitives/numbers.rs +++ b/crates/steel-core/src/primitives/numbers.rs @@ -1058,7 +1058,7 @@ fn numerator(number: &SteelVal) -> Result { /// /// (floor number) -> number? /// -/// * number : number? - The number to compute the floor for. +/// * number : real? - The number to compute the floor for. /// /// # Examples /// ```scheme @@ -1082,7 +1082,7 @@ fn floor(number: &SteelVal) -> Result { /// /// (ceiling number) -> integer? /// -/// * number : number? - The number to round up. +/// * number : real? - The number to round up. /// /// # Examples /// ```scheme @@ -1106,7 +1106,7 @@ fn ceiling(number: &SteelVal) -> Result { /// /// (truncate number) -> integer? /// -/// * number : number? - The number to truncate. +/// * number : real? - The number to truncate. /// /// # Examples /// @@ -1178,7 +1178,7 @@ where /// /// (round number) -> number? /// -/// * number : number? - The number to round. +/// * number : real? - The number to round. /// /// # Examples /// ```scheme From 98ffe277f68607fc68056aeffe5244003b281c40 Mon Sep 17 00:00:00 2001 From: may Date: Tue, 23 Sep 2025 23:12:48 +0200 Subject: [PATCH 8/8] cargo xtask docgen --- docs/src/builtins/steel_base.md | 31 +++++++++++++++++++++++------- docs/src/builtins/steel_numbers.md | 31 +++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/docs/src/builtins/steel_base.md b/docs/src/builtins/steel_base.md index 6fbec4519..cca9f12fd 100644 --- a/docs/src/builtins/steel_base.md +++ b/docs/src/builtins/steel_base.md @@ -432,7 +432,7 @@ Rounds the given number up to the nearest integer not less than it. (ceiling number) -> integer? -* number : number? - The number to round up. +* number : real? - The number to round up. #### Examples ```scheme @@ -1068,11 +1068,11 @@ Checks if the given value is a floating-point number > (float? #t) ;; => #f ``` ### **floor** -Computes the largest integer less than or equal to the given number. +Rounds the given number down to the nearest integer not larger than it. (floor number) -> number? -* number : number? - The number to compute the floor for. +* number : real? - The number to compute the floor for. #### Examples ```scheme @@ -2476,17 +2476,20 @@ This function takes time proportional to the length of `lst`. > (reverse (list 1 2 3 4)) ;; '(4 3 2 1) ``` ### **round** -Rounds the given number to the nearest integer. +Rounds the given number to the nearest integer, rounding half-way cases to +the even number. (round number) -> number? -* number : number? - The number to round. +* number : real? - The number to round. #### Examples ```scheme > (round 3.14) ;; => 3 > (round 4.6) ;; => 5 -> (round -2.5) ;; => -3 +> (round 2.5) ;; => 2 +> (round 3.5) ;; => 4 +> (round -2.5) ;; => -2 ``` ### **second** Get the second element of the list. Raises an error if the list does not have an element in the second position. @@ -3133,6 +3136,21 @@ of the string ```scheme > (trim-start-matches "123foo1bar123123" "123") ;; => "foo1bar123123" ``` +### **truncate** +Rounds the given number to the nearest integer, whose absolute value is not +larger than it. + +(truncate number) -> integer? + +* number : real? - The number to truncate. + +#### Examples + +```scheme +> (truncate 42) ;; => 42 +> (truncate 42.1) ;; => 42 +> (truncate -42.1) ;; => -42 +``` ### **utf8->string** Alias of `bytes->string/utf8`. ### **utf8-length** @@ -3566,7 +3584,6 @@ Create a zipping iterator ### **thread/available-parallelism** ### **thread::current/id** ### **transduce** -### **truncate** ### **try-list-ref** ### **unbox** ### **unbox-strong** diff --git a/docs/src/builtins/steel_numbers.md b/docs/src/builtins/steel_numbers.md index 1afbfbd82..cd913afc6 100644 --- a/docs/src/builtins/steel_numbers.md +++ b/docs/src/builtins/steel_numbers.md @@ -132,7 +132,7 @@ Rounds the given number up to the nearest integer not less than it. (ceiling number) -> integer? -* number : number? - The number to round up. +* number : real? - The number to round up. #### Examples ```scheme @@ -276,11 +276,11 @@ Returns `#t` if the given number is finite. > (finite? +nan.0) ;; => #f ``` ### **floor** -Computes the largest integer less than or equal to the given number. +Rounds the given number down to the nearest integer not larger than it. (floor number) -> number? -* number : number? - The number to compute the floor for. +* number : real? - The number to compute the floor for. #### Examples ```scheme @@ -499,17 +499,20 @@ This differs from the modulo operator when using negative numbers. > (remainder -10 -3) ;; => -1 ``` ### **round** -Rounds the given number to the nearest integer. +Rounds the given number to the nearest integer, rounding half-way cases to +the even number. (round number) -> number? -* number : number? - The number to round. +* number : real? - The number to round. #### Examples ```scheme > (round 3.14) ;; => 3 > (round 4.6) ;; => 5 -> (round -2.5) ;; => -3 +> (round 2.5) ;; => 2 +> (round 3.5) ;; => 4 +> (round -2.5) ;; => -2 ``` ### **sin** Returns the sine value of the input angle, measured in radians. @@ -565,6 +568,21 @@ Returns the tangent value of the input angle, measured in radians. > (tan 2.0) ;; => -2.185039863261519 > (tan 3.14) ;; => -0.0015926549364072232 ``` +### **truncate** +Rounds the given number to the nearest integer, whose absolute value is not +larger than it. + +(truncate number) -> integer? + +* number : real? - The number to truncate. + +#### Examples + +```scheme +> (truncate 42) ;; => 42 +> (truncate 42.1) ;; => 42 +> (truncate -42.1) ;; => -42 +``` ### **zero?** Checks if the given real number is zero. @@ -578,4 +596,3 @@ Checks if the given real number is zero. > (zero? 0.0) ;; => #t > (zero? 0.1) ;; => #f ``` -### **truncate**