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
189 changes: 97 additions & 92 deletions .gas-snapshot

Large diffs are not rendered by default.

25 changes: 9 additions & 16 deletions crates/float/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,11 @@ impl Float {
/// ```
pub fn format(self) -> Result<String, FloatError> {
let Float(a) = self;
let calldata = DecimalFloat::formatCall { a }.abi_encode();
let calldata = DecimalFloat::formatCall {
a,
sigFigsLimit: U256::from(9),
}
.abi_encode();

execute_call(Bytes::from(calldata), |output| {
let decoded = DecimalFloat::formatCall::abi_decode_returns(output.as_ref())?;
Expand Down Expand Up @@ -1016,17 +1020,6 @@ mod tests {
}
}

#[test]
fn test_parse_and_format() {
let float = Float::parse("1.1341234234625468391".to_string()).unwrap();
let err = float.format().unwrap_err();

assert!(matches!(
err,
FloatError::DecimalFloat(DecimalFloatErrors::LossyConversionFromFloat(_))
));
}

#[test]
fn test_parse_empty_string_error() {
let err = Float::parse("".to_string()).unwrap_err();
Expand Down Expand Up @@ -1223,12 +1216,12 @@ mod tests {
let float = Float::parse("-3613.1324123".to_string()).unwrap();
let abs = float.abs().unwrap();
let formatted = abs.format().unwrap();
assert_eq!(formatted, "3613.1324123");
assert_eq!(formatted, "3.6131324123e3");

let float = Float::parse("3613.1324123".to_string()).unwrap();
let abs = float.abs().unwrap();
let formatted = abs.format().unwrap();
assert_eq!(formatted, "3613.1324123");
assert_eq!(formatted, "3.6131324123e3");

let float = Float::parse("0".to_string()).unwrap();
let abs = float.abs().unwrap();
Expand All @@ -1248,12 +1241,12 @@ mod tests {
let float = Float::parse("-123.1234234625468391".to_string()).unwrap();
let negated = float.neg().unwrap();
let formatted = negated.format().unwrap();
assert_eq!(formatted, "123.1234234625468391");
assert_eq!(formatted, "1.231234234625468391e2");

let float = Float::parse(formatted).unwrap();
let negated = float.neg().unwrap();
let formatted = negated.format().unwrap();
assert_eq!(formatted, "-123.1234234625468391");
assert_eq!(formatted, "-1.231234234625468391e2");

let float = Float::parse("0".to_string()).unwrap();
let negated = float.neg().unwrap();
Expand Down
5 changes: 3 additions & 2 deletions src/concrete/DecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ contract DecimalFloat {

/// Exposes `LibFormatDecimalFloat.toDecimalString` for offchain use.
/// @param a The float to format.
/// @param sigFigsLimit The significant figures limit.
/// @return The string representation of the float.
function format(Float a) external pure returns (string memory) {
return LibFormatDecimalFloat.toDecimalString(a);
function format(Float a, uint256 sigFigsLimit) external pure returns (string memory) {
return LibFormatDecimalFloat.toDecimalString(a, sigFigsLimit);
}
Comment thread
thedavidmeister marked this conversation as resolved.

/// Exposes `LibDecimalFloat.add` for offchain use.
Expand Down
122 changes: 111 additions & 11 deletions src/lib/format/LibFormatDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,123 @@ pragma solidity ^0.8.25;
import {LibDecimalFloat, Float} from "../LibDecimalFloat.sol";

import {LibFixedPointDecimalFormat} from "rain.math.fixedpoint/lib/format/LibFixedPointDecimalFormat.sol";
import {LibDecimalFloatImplementation} from "../../lib/implementation/LibDecimalFloatImplementation.sol";

import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";
Comment thread
thedavidmeister marked this conversation as resolved.

library LibFormatDecimalFloat {
function countSigFigs(int256 signedCoefficient, int256 exponent) internal pure returns (uint256) {
if (signedCoefficient == 0) {
return 1;
}

uint256 sigFigs = 0;

if (exponent < 0) {
while (signedCoefficient % 10 == 0) {
signedCoefficient /= 10;
exponent++;
}
}

while (signedCoefficient != 0) {
sigFigs++;
signedCoefficient /= 10;
}

// Adjust for exponent
if (exponent < 0) {
exponent = -exponent;
sigFigs = sigFigs > uint256(exponent) ? sigFigs : uint256(exponent);
} else if (exponent > 0) {
sigFigs += uint256(exponent);
}

return sigFigs;
}

/// Format a decimal float as a string.
/// Currently is a thin wrapper around converting to a fixed point decimal
/// and then formatting that as a string.
/// In the future this may be extended to support a wider range of possible
/// values.
/// Not particularly efficient as it is intended for offchain use that
/// doesn't cost gas.
Comment thread
thedavidmeister marked this conversation as resolved.
/// @param float The decimal float to format.
/// @return The string representation of the decimal float.
function toDecimalString(Float float) internal pure returns (string memory) {
//slither-disable-next-line cyclomatic-complexity
function toDecimalString(Float float, uint256 sigFigsLimit) internal pure returns (string memory) {
Comment thread
thedavidmeister marked this conversation as resolved.
(int256 signedCoefficient, int256 exponent) = LibDecimalFloat.unpack(float);
string memory prefix = "";
if (signedCoefficient < 0) {
prefix = "-";
signedCoefficient = -signedCoefficient;
if (signedCoefficient == 0) {
return "0";
}

uint256 sigFigs = countSigFigs(signedCoefficient, exponent);
bool scientific = sigFigs > sigFigsLimit;
uint256 scaleExponent;
Comment thread
thedavidmeister marked this conversation as resolved.
uint256 scale = 0;
if (scientific) {
(signedCoefficient, exponent) = LibDecimalFloatImplementation.maximize(signedCoefficient, exponent);

bool isAtLeastE76 = signedCoefficient / 1e76 != 0;
scaleExponent = isAtLeastE76 ? uint256(76) : uint256(75);
scale = uint256(10) ** scaleExponent;
} else {
if (exponent > 0) {
signedCoefficient *= int256(10) ** uint256(exponent);
exponent = 0;
}
if (exponent < 0) {
scale = uint256(10) ** uint256(-exponent);
scaleExponent = uint256(-exponent);
} else {
scaleExponent = uint256(exponent);
}
}
uint256 decimal18Value = LibDecimalFloat.toFixedDecimalLossless(signedCoefficient, exponent, 18);
return string.concat(prefix, LibFixedPointDecimalFormat.fixedPointToDecimalString(decimal18Value));

int256 integral = scale != 0 ? signedCoefficient / int256(scale) : signedCoefficient;
int256 fractional = scale != 0 ? signedCoefficient % int256(scale) : int256(0);
bool isNeg = false;
if (integral < 0) {
isNeg = true;
integral = -integral;
}
if (fractional < 0) {
isNeg = true;
fractional = -fractional;
}

string memory fractionalString = "";
{
string memory fracLeadingZerosString = "";

if (fractional != 0) {
uint256 fracLeadingZeros = 0;
uint256 fracScale = scale / 10;
while (fractional / int256(fracScale) == 0) {
fracScale /= 10;
fracLeadingZeros++;
}

for (uint256 i = 0; i < fracLeadingZeros; i++) {
fracLeadingZerosString = string.concat(fracLeadingZerosString, "0");
}

Comment thread
thedavidmeister marked this conversation as resolved.
while (fractional % 10 == 0) {
fractional /= 10;
}
}

fractionalString =
fractional == 0 ? "" : string.concat(".", fracLeadingZerosString, Strings.toString(fractional));
Comment thread
thedavidmeister marked this conversation as resolved.
}

string memory integralString = Strings.toString(integral);

int256 displayExponent = exponent + int256(scaleExponent);
string memory exponentString =
(displayExponent == 0 || !scientific) ? "" : string.concat("e", Strings.toString(displayExponent));

string memory prefix = isNeg ? "-" : "";

string memory fullString = string.concat(prefix, integralString, fractionalString, exponentString);

return fullString;
}
}
7 changes: 5 additions & 2 deletions src/lib/parse/LibParseDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,15 @@ library LibParseDecimalFloat {
// fractional part.
exponent = int256(fracStart) - int256(nonZeroCursor);
uint256 scale = uint256(-exponent);
if (scale >= 67 && signedCoefficient != 0) {
if (scale > 67 && signedCoefficient != 0) {
return (ParseDecimalPrecisionLoss.selector, cursor, 0, 0);
}
scale = 10 ** scale;
int256 rescaledIntValue = signedCoefficient * int256(scale);
if (rescaledIntValue / int256(scale) != signedCoefficient) {
if (
rescaledIntValue / int256(scale) != signedCoefficient
|| int224(rescaledIntValue) != rescaledIntValue
) {
return (ParseDecimalPrecisionLoss.selector, cursor, 0, 0);
}
signedCoefficient = rescaledIntValue + fracValue;
Expand Down
12 changes: 6 additions & 6 deletions test/src/concrete/DecimalFloat.format.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ import {LibFormatDecimalFloat} from "src/lib/format/LibFormatDecimalFloat.sol";
contract DecimalFloatFormatTest is Test {
using LibDecimalFloat for Float;

function formatExternal(Float a) external pure returns (string memory) {
return LibFormatDecimalFloat.toDecimalString(a);
function formatExternal(Float a, uint256 sigFigsLimit) external pure returns (string memory) {
return LibFormatDecimalFloat.toDecimalString(a, sigFigsLimit);
}

function testFormatDeployed(Float a) external {
function testFormatDeployed(Float a, uint256 sigFigsLimit) external {
DecimalFloat deployed = new DecimalFloat();

try this.formatExternal(a) returns (string memory str) {
string memory deployedStr = deployed.format(a);
try this.formatExternal(a, sigFigsLimit) returns (string memory str) {
string memory deployedStr = deployed.format(a, sigFigsLimit);

assertEq(str, deployedStr);
} catch (bytes memory err) {
vm.expectRevert(err);
deployed.format(a);
deployed.format(a, sigFigsLimit);
}
Comment thread
thedavidmeister marked this conversation as resolved.
}
}
123 changes: 123 additions & 0 deletions test/src/lib/format/LibFormatDecimalFloat.countSigFigs.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 thedavidmeister
pragma solidity =0.8.25;

import {Test} from "forge-std/Test.sol";

import {LibFormatDecimalFloat} from "src/lib/format/LibFormatDecimalFloat.sol";

/// @title LibFormatDecimalFloatCountSigFigs
contract LibFormatDecimalFloatCountSigFigs is Test {
function checkCountSigFigs(int256 signedCoefficient, int256 exponent, uint256 expected) internal pure {
uint256 actual = LibFormatDecimalFloat.countSigFigs(signedCoefficient, exponent);
assertEq(actual, expected, "Unexpected significant figures count");
}

function testCountSigFigsExamples() external pure {
checkCountSigFigs(0, 0, 1);

// 1 = 1
checkCountSigFigs(1, 0, 1);
checkCountSigFigs(10, -1, 1);

// -1 = 1
checkCountSigFigs(-1, 0, 1);
checkCountSigFigs(-10, -1, 1);

// 10 = 2
checkCountSigFigs(10, 0, 2);
checkCountSigFigs(100, -1, 2);

// -10 = 2
checkCountSigFigs(-10, 0, 2);
checkCountSigFigs(-100, -1, 2);

// 0.1 = 1
checkCountSigFigs(1, -1, 1);
checkCountSigFigs(10, -2, 1);

// -0.1 = 1
checkCountSigFigs(-1, -1, 1);
checkCountSigFigs(-10, -2, 1);

// 0.01 = 2
checkCountSigFigs(1, -2, 2);
checkCountSigFigs(10, -3, 2);

// -0.01 = 2
checkCountSigFigs(-1, -2, 2);
checkCountSigFigs(-10, -3, 2);

// 0.001 = 3
checkCountSigFigs(1, -3, 3);
checkCountSigFigs(10, -4, 3);

// -0.001 = 3
checkCountSigFigs(-1, -3, 3);
checkCountSigFigs(-10, -4, 3);

// 1.1 = 2
checkCountSigFigs(11, -1, 2);
checkCountSigFigs(110, -2, 2);

// -1.1 = 2
checkCountSigFigs(-11, -1, 2);
checkCountSigFigs(-110, -2, 2);

// 1.01 = 3
checkCountSigFigs(101, -2, 3);
checkCountSigFigs(1010, -3, 3);

// -1.01 = 3
checkCountSigFigs(-101, -2, 3);
checkCountSigFigs(-1010, -3, 3);

// 10.1 = 3
checkCountSigFigs(101, -1, 3);
checkCountSigFigs(1010, -2, 3);

// -10.1 = 3
checkCountSigFigs(-101, -1, 3);
checkCountSigFigs(-1010, -2, 3);

// 10.01 = 4
checkCountSigFigs(1001, -2, 4);
checkCountSigFigs(10010, -3, 4);

// -10.01 = 4
checkCountSigFigs(-1001, -2, 4);
checkCountSigFigs(-10010, -3, 4);

// internal zeros are significant
checkCountSigFigs(100100, 0, 6);
checkCountSigFigs(-100100, 0, 6);

// trailing zeros without decimal are significant
checkCountSigFigs(100, 0, 3);
checkCountSigFigs(1000, 0, 4);

// trailing zeros after decimal are not significant
// 1.00 and 0.00100
checkCountSigFigs(100, -2, 1);
checkCountSigFigs(100, -5, 3);

// positive exponent growth
// 10
checkCountSigFigs(1, 1, 2);
// 100
checkCountSigFigs(1, 2, 3);
// -1000
checkCountSigFigs(-1, 3, 4);
}

function testCountSigFigsZero(int256 exponent) external pure {
checkCountSigFigs(0, exponent, 1);
}

function testCountSigFigsOne(int256 exponent) external pure {
exponent = bound(exponent, -76, 0);
int256 one = int256(10 ** uint256(-exponent));
checkCountSigFigs(one, exponent, 1);
checkCountSigFigs(-one, exponent, 1);
}
Comment thread
thedavidmeister marked this conversation as resolved.
}
Loading
Loading