Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
{
"name": "math features",
"steps": [
{
"step": "setState",
"accounts": {
"sc:basic-features": {
"nonce": "0",
"balance": "0",
"code": "mxsc:../output/basic-features.mxsc.json"
},
"address:an_account": {
"nonce": "0",
"balance": "0"
}
}
},
{
"step": "scCall",
"id": "weighted_average_equal_weights",
"comment": "(10*1 + 20*1) / (1+1) = 15",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"function": "math_weighted_average",
"arguments": ["10", "1", "20", "1"],
"gasLimit": "50,000,000",
"gasPrice": "0"
},
"expect": {
"out": ["15"],
"status": "0"
}
},
{
"step": "scCall",
"id": "weighted_average_unequal_weights",
"comment": "(0*3 + 30*7) / (3+7) = 21",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"function": "math_weighted_average",
"arguments": ["0", "3", "30", "7"],
"gasLimit": "50,000,000",
"gasPrice": "0"
},
"expect": {
"out": ["21"],
"status": "0"
}
},
{
"step": "scCall",
"id": "weighted_average_truncates",
"comment": "(0*1 + 10*3) / (1+3) = 30/4 = 7 (truncated)",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"function": "math_weighted_average",
"arguments": ["0", "1", "10", "3"],
"gasLimit": "50,000,000",
"gasPrice": "0"
},
"expect": {
"out": ["7"],
"status": "0"
}
},
{
"step": "scCall",
"id": "weighted_average_round_up_equal_weights",
"comment": "(10*1 + 20*1) / (1+1) = 15 (exact, no rounding)",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"function": "math_weighted_average_round_up",
"arguments": ["10", "1", "20", "1"],
"gasLimit": "50,000,000",
"gasPrice": "0"
},
"expect": {
"out": ["15"],
"status": "0"
}
},
{
"step": "scCall",
"id": "weighted_average_round_up_rounds_up",
"comment": "(0*1 + 10*3 + 4 - 1) / (1+3) = 33/4 = 8 (rounded up from 7.5)",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"function": "math_weighted_average_round_up",
"arguments": ["0", "1", "10", "3"],
"gasLimit": "50,000,000",
"gasPrice": "0"
},
"expect": {
"out": ["8"],
"status": "0"
}
},
{
"step": "scCall",
"id": "weighted_average_round_up_exact",
"comment": "(1*1 + 4*2) / (1+2) = 9/3 = 3",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"function": "math_weighted_average_round_up",
"arguments": ["1", "1", "4", "2"],
"gasLimit": "50,000,000",
"gasPrice": "0"
},
"expect": {
"out": ["3"],
"status": "0"
}
},
{
"step": "scCall",
"id": "linear_interpolation_midpoint",
"comment": "(0*(100-50) + 200*(50-0)) / (100-0) = 10000/100 = 100",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"function": "math_linear_interpolation",
"arguments": ["0", "100", "50", "0", "200"],
"gasLimit": "50,000,000",
"gasPrice": "0"
},
"expect": {
"out": ["100"],
"status": "0"
}
},
{
"step": "scCall",
"id": "linear_interpolation_at_min",
"comment": "current_in == min_in => returns min_out = 5",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"function": "math_linear_interpolation",
"arguments": ["0", "10", "0", "5", "15"],
"gasLimit": "50,000,000",
"gasPrice": "0"
},
"expect": {
"out": ["5"],
"status": "0"
}
},
{
"step": "scCall",
"id": "linear_interpolation_at_max",
"comment": "current_in == max_in => returns max_out = 15",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"function": "math_linear_interpolation",
"arguments": ["0", "10", "10", "5", "15"],
"gasLimit": "50,000,000",
"gasPrice": "0"
},
"expect": {
"out": ["15"],
"status": "0"
}
},
{
"step": "scCall",
"id": "linear_interpolation_out_of_range",
"comment": "current_in > max_in => sc_panic",
"tx": {
"from": "address:an_account",
"to": "sc:basic-features",
"function": "math_linear_interpolation",
"arguments": ["0", "10", "11", "5", "15"],
"gasLimit": "50,000,000",
"gasPrice": "0"
},
"expect": {
"out": [],
"status": "4",
"message": "str:current_in out of [min_in, max_in] range",
"gas": "*",
"refund": "*"
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod managed_buffer_features;
pub mod managed_decimal_features;
pub mod managed_map_features;
pub mod managed_vec_features;
pub mod math_features;
pub mod non_zero_features;
pub mod small_num_overflow_test_ops;
pub mod special_roles_from_system_account;
Expand Down Expand Up @@ -90,6 +91,7 @@ pub trait BasicFeatures:
+ storage_mapper_get_at_address::StorageMapperGetAtAddress
+ managed_decimal_features::ManagedDecimalFeatures
+ managed_map_features::ManagedMapFeatures
+ math_features::MathFeatures
{
#[init]
fn init(&self) {}
Expand Down
40 changes: 40 additions & 0 deletions contracts/feature-tests/basic-features/src/math_features.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use multiversx_sc::imports::*;
use multiversx_sc::math;

#[multiversx_sc::module]
pub trait MathFeatures {
#[endpoint]
fn math_weighted_average(
&self,
first_value: BigUint,
first_weight: BigUint,
second_value: BigUint,
second_weight: BigUint,
) -> BigUint {
math::weighted_average(first_value, first_weight, second_value, second_weight)
}

#[endpoint]
fn math_weighted_average_round_up(
&self,
first_value: BigUint,
first_weight: BigUint,
second_value: BigUint,
second_weight: BigUint,
) -> BigUint {
math::weighted_average_round_up(first_value, first_weight, second_value, second_weight)
}

#[endpoint]
fn math_linear_interpolation(
&self,
min_in: BigUint,
max_in: BigUint,
current_in: BigUint,
min_out: BigUint,
max_out: BigUint,
) -> BigUint {
math::linear_interpolation(min_in, max_in, current_in, min_out, max_out)
.unwrap_or_else(|_| sc_panic!("current_in out of [min_in, max_in] range"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ fn managed_vec_biguint_push_go() {
world().run("scenarios/managed_vec_biguint_push.scen.json");
}

#[test]
fn math_features_go() {
world().run("scenarios/math_features.scen.json");
}

#[test]
fn mmap_get_go() {
world().run("scenarios/mmap_get.scen.json");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,11 @@ fn managed_vec_biguint_push_rs() {
world().run("scenarios/managed_vec_biguint_push.scen.json");
}

#[test]
fn math_features_rs() {
world().run("scenarios/math_features.scen.json");
}

#[test]
fn mmap_get_rs() {
world().run("scenarios/mmap_get.scen.json");
Expand Down
1 change: 1 addition & 0 deletions framework/base/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub mod hex_call_data;
pub mod io;
pub mod log_util;
mod macros;
pub mod math;
pub mod non_zero_util;
pub mod storage;
pub mod tuple_util;
Expand Down
7 changes: 7 additions & 0 deletions framework/base/src/math.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// Only used internally for computing logarithms for ManagedDecimal and BigUint.
pub(crate) mod internal_logarithm_i64;
mod linear_interpolation;
mod weighted_average;

pub use linear_interpolation::{LinearInterpolationInvalidValuesError, linear_interpolation};
pub use weighted_average::{weighted_average, weighted_average_round_up};
40 changes: 40 additions & 0 deletions framework/base/src/math/linear_interpolation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use core::ops::{Add, Div, Mul, Sub};

/// Error returned when `current_in` is outside the `[min_in, max_in]` range.
#[derive(Debug)]
pub struct LinearInterpolationInvalidValuesError;

/// Computes a linearly interpolated output value for a given input within a known range.
///
/// Given an input range `[min_in, max_in]` and a corresponding output range `[min_out, max_out]`,
/// maps `current_in` proportionally to its position in the output range.
///
/// Formula:
/// ```text
/// out = (min_out * (max_in - current_in) + max_out * (current_in - min_in)) / (max_in - min_in)
/// ```
///
/// Returns [`Err(LinearInterpolationInvalidValuesError)`] if `current_in` is outside `[min_in, max_in]`.
///
/// See also: <https://en.wikipedia.org/wiki/Linear_interpolation>
pub fn linear_interpolation<T>(
min_in: T,
max_in: T,
current_in: T,
min_out: T,
max_out: T,
) -> Result<T, LinearInterpolationInvalidValuesError>
where
T: Add<Output = T> + Sub<Output = T> + Mul<Output = T> + Div<Output = T> + PartialOrd + Clone,
{
if min_in > max_in || current_in < min_in || current_in > max_in {
return Err(LinearInterpolationInvalidValuesError);
}

let min_out_weighted = min_out * (max_in.clone() - current_in.clone());
let max_out_weighted = max_out * (current_in - min_in.clone());
let in_diff = max_in - min_in;

let result = (min_out_weighted + max_out_weighted) / in_diff;
Ok(result)
}
39 changes: 39 additions & 0 deletions framework/base/src/math/weighted_average.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use core::ops::{Add, Div, Mul, Sub};

/// Computes the weighted average of two values.
///
/// Returns `(first_value * first_weight + second_value * second_weight) / (first_weight + second_weight)`.
///
/// # Panics
///
/// Panics on division by zero if both weights are zero.
pub fn weighted_average<T>(first_value: T, first_weight: T, second_value: T, second_weight: T) -> T
where
T: Add<Output = T> + Mul<Output = T> + Div<Output = T> + Clone,
{
let weight_sum = first_weight.clone() + second_weight.clone();
let weighted_sum = first_value * first_weight + second_value * second_weight;
weighted_sum / weight_sum
}

/// Computes the weighted average of two values, rounded up (ceiling division).
///
/// Equivalent to [`weighted_average`], but rounds the result up instead of truncating:
/// `(weighted_sum + weight_sum - 1) / weight_sum`.
///
/// # Panics
///
/// Panics on division by zero if both weights are zero.
pub fn weighted_average_round_up<T>(
first_value: T,
first_weight: T,
second_value: T,
second_weight: T,
) -> T
where
T: Add<Output = T> + Sub<Output = T> + Mul<Output = T> + Div<Output = T> + Clone + From<u32>,
{
let weight_sum = first_weight.clone() + second_weight.clone();
let weighted_sum = first_value * first_weight + second_value * second_weight;
(weighted_sum + weight_sum.clone() - T::from(1u32)) / weight_sum
}
1 change: 0 additions & 1 deletion framework/base/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pub mod heap;
mod interaction;
mod io;
mod managed;
pub(crate) mod math_util;
mod static_buffer;

pub use crypto::*;
Expand Down
Loading
Loading