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
27 changes: 27 additions & 0 deletions crates/float/src/js_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,33 @@ impl Float {
Self::from_fixed_decimal(val, decimals)
}

/// Converts a `Float` to a fixed-point decimal value using the specified number of decimals.
///
/// # Arguments
///
/// * `decimals` - The number of decimals in the fixed-point representation.
///
/// # Returns
///
/// * `Ok(String)` - The resulting fixed-point decimal value as a string.
/// * `Err(FloatError)` - If the conversion fails.
///
/// # Example
///
/// ```typescript
/// const float = Float.parse("123.45").value!;
/// const result = float.toFixedDecimal(2);
/// if (result.error) {
/// console.error(result.error);
/// }
/// assert(result.value === "12345");
/// ```
#[wasm_export(js_name = "toFixedDecimal")]
pub fn to_fixed_decimal_js(&self, decimals: u8) -> Result<String, FloatError> {
let fixed = self.to_fixed_decimal(decimals)?;
Ok(fixed.to_string())
}

/// Packs a coefficient and exponent into a `Float` in a lossless manner.
///
/// # Arguments
Expand Down
58 changes: 57 additions & 1 deletion crates/float/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,41 @@ impl Float {
})
}

/// Converts a `Float` to a fixed-point decimal value using the specified number of decimals.
///
/// # Arguments
///
/// * `decimals` - The number of decimals in the fixed-point representation.
///
/// # Returns
///
/// * `Ok(U256)` - The resulting fixed-point decimal value.
/// * `Err(FloatError)` - If the conversion fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
/// use alloy::primitives::U256;
///
/// // 123.45 with 2 decimals becomes 12345
/// let float = Float::parse("123.45".to_string())?;
/// let fixed = float.to_fixed_decimal(2)?;
/// assert_eq!(fixed, U256::from(12345u64));
///
/// anyhow::Ok(())
/// ```
pub fn to_fixed_decimal(self, decimals: u8) -> Result<U256, FloatError> {
let Float(float) = self;
let calldata = DecimalFloat::toFixedDecimalLosslessCall { float, decimals }.abi_encode();

execute_call(Bytes::from(calldata), |output| {
let decoded =
DecimalFloat::toFixedDecimalLosslessCall::abi_decode_returns(output.as_ref())?;
Ok(decoded)
})
}

/// Packs a coefficient and exponent into a `Float` in a lossless manner.
///
/// # Arguments
Expand Down Expand Up @@ -1304,6 +1339,24 @@ mod tests {
));
}

#[test]
fn test_to_fixed_decimal() {
let cases = vec![
("0", 0u8, 0u128),
("0", 18u8, 0u128),
("1e-18", 18u8, 1u128),
("123456789", 0u8, 123456789u128),
("123456789e-2", 2u8, 123456789u128),
("1", 18u8, 1000000000000000000u128),
];

for (input, decimals, expected) in cases {
let float = Float::parse(input.to_string()).unwrap();
let fixed = float.to_fixed_decimal(decimals).unwrap();
assert_eq!(fixed, U256::from(expected));
}
}

#[test]
fn test_frac_and_floor_integers() {
let int_float = Float::parse("12345".to_string()).unwrap();
Expand Down Expand Up @@ -1341,7 +1394,7 @@ mod tests {

proptest! {
#[test]
fn test_from_fixed_decimal_valid_range(coeff in any::<I224>(), decimals in 0u8..=66u8) {
fn test_from_to_fixed_decimal_valid_range(coeff in any::<I224>(), decimals in 0u8..=66u8) {
prop_assume!(coeff >= I224::ZERO);

let exponent = -(decimals as i32);
Expand All @@ -1350,6 +1403,9 @@ mod tests {
let float = Float::from_fixed_decimal(value, decimals).unwrap();
let expected = Float::pack_lossless(coeff, exponent).unwrap();
prop_assert!(float.eq(expected).unwrap());

let fixed = float.to_fixed_decimal(decimals).unwrap();
assert_eq!(fixed, value);
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/concrete/DecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,13 @@ contract DecimalFloat {
function fromFixedDecimalLosslessPacked(uint256 value, uint8 decimals) external pure returns (Float) {
return LibDecimalFloat.fromFixedDecimalLosslessPacked(value, decimals);
}

/// Exposes `LibDecimalFloat.toFixedDecimalLossless` for offchain use.
/// @param float The Float struct to convert.
/// @param decimals The number of decimals in the fixed point
/// representation. e.g. If 1e18 represents 1 this would be 18 decimals.
/// @return The fixed point decimal value as a uint256.
function toFixedDecimalLossless(Float float, uint8 decimals) external pure returns (uint256) {
return LibDecimalFloat.toFixedDecimalLossless(float, decimals);
}
}
13 changes: 13 additions & 0 deletions test_js/float.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ describe('Test Float Bindings', () => {
expect(float.format18()?.value!).toBe('123.45');
});

it('should test toFixedDecimal', () => {
const float = Float.parse('123.45')?.value!;
expect(float.toFixedDecimal(2)?.value!).toBe('12345');
});

it('should test toFixedDecimal roundtrip', () => {
const originalValue = '9876543210';
const decimals = 8;
const float = Float.fromFixedDecimal(originalValue, decimals)?.value!;
const result = float.toFixedDecimal(decimals)?.value!;
expect(result).toBe(originalValue);
});

it('should test packLossless', () => {
const float = Float.packLossless('314', -2)?.value!;
expect(float.format()?.value!).toBe('3.14');
Expand Down
Loading