diff --git a/.gas-snapshot b/.gas-snapshot index 02f70912..7d657b19 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,36 +1,37 @@ -DecimalFloatAbsTest:testAbsDeployed(bytes32) (runs: 5096, μ: 3611684, ~: 3611622) -DecimalFloatAddTest:testAddDeployed(bytes32,bytes32) (runs: 5096, μ: 3617219, ~: 3617323) -DecimalFloatCeilTest:testCeilDeployed(bytes32) (runs: 5096, μ: 3611687, ~: 3611285) -DecimalFloatConstantsTest:testEDeployed() (gas: 3610815) -DecimalFloatConstantsTest:testMaxNegativeValueDeployed() (gas: 3610804) -DecimalFloatConstantsTest:testMaxPositiveValueDeployed() (gas: 3610805) -DecimalFloatConstantsTest:testMinNegativeValueDeployed() (gas: 3610780) -DecimalFloatConstantsTest:testMinPositiveValueDeployed() (gas: 3610781) -DecimalFloatConstantsTest:testZeroDeployed() (gas: 3610848) -DecimalFloatDivTest:testDivDeployed(bytes32,bytes32) (runs: 5096, μ: 3618767, ~: 3618901) -DecimalFloatEqTest:testEqDeployed(bytes32,bytes32) (runs: 5096, μ: 3612016, ~: 3611940) -DecimalFloatFloorTest:testFloorDeployed(bytes32) (runs: 5096, μ: 3611475, ~: 3611283) -DecimalFloatFormatTest:testFormatDeployed(bytes32,uint256) (runs: 5096, μ: 3637333, ~: 3641988) -DecimalFloatFracTest:testFracDeployed(bytes32) (runs: 5096, μ: 3611869, ~: 3611833) -DecimalFloatFromFixedDecimalLosslessTest:testFromFixedDecimalLosslessDeployed(uint256,uint8) (runs: 5096, μ: 3612455, ~: 3612401) -DecimalFloatFromFixedDecimalLossyTest:testFromFixedDecimalLossyDeployed(uint256,uint8) (runs: 5096, μ: 3612932, ~: 3612868) -DecimalFloatGtTest:testGtDeployed(bytes32,bytes32) (runs: 5096, μ: 3611954, ~: 3611882) -DecimalFloatGteTest:testGteDeployed(bytes32,bytes32) (runs: 5096, μ: 3611941, ~: 3611868) -DecimalFloatInvTest:testInvDeployed(bytes32) (runs: 5096, μ: 3617158, ~: 3617217) -DecimalFloatIsZeroTest:testIsZeroDeployed(bytes32) (runs: 5096, μ: 3611125, ~: 3611125) -DecimalFloatLtTest:testLtDeployed(bytes32,bytes32) (runs: 5096, μ: 3611931, ~: 3611859) -DecimalFloatLteTest:testLteDeployed(bytes32,bytes32) (runs: 5096, μ: 3611985, ~: 3611912) -DecimalFloatMaxTest:testMaxDeployed(bytes32,bytes32) (runs: 5096, μ: 3611994, ~: 3611934) -DecimalFloatMinTest:testMinDeployed(bytes32,bytes32) (runs: 5096, μ: 3611992, ~: 3611932) -DecimalFloatMinusTest:testMinusDeployed(bytes32) (runs: 5096, μ: 3611790, ~: 3611791) -DecimalFloatMulTest:testMulDeployed(bytes32,bytes32) (runs: 5096, μ: 3615714, ~: 3616500) +DecimalFloatAbsTest:testAbsDeployed(bytes32) (runs: 5096, μ: 3678691, ~: 3678629) +DecimalFloatAddTest:testAddDeployed(bytes32,bytes32) (runs: 5096, μ: 3684124, ~: 3684193) +DecimalFloatCeilTest:testCeilDeployed(bytes32) (runs: 5096, μ: 3678607, ~: 3678205) +DecimalFloatConstantsTest:testEDeployed() (gas: 3677779) +DecimalFloatConstantsTest:testMaxNegativeValueDeployed() (gas: 3677769) +DecimalFloatConstantsTest:testMaxPositiveValueDeployed() (gas: 3677770) +DecimalFloatConstantsTest:testMinNegativeValueDeployed() (gas: 3677745) +DecimalFloatConstantsTest:testMinPositiveValueDeployed() (gas: 3677768) +DecimalFloatConstantsTest:testZeroDeployed() (gas: 3677790) +DecimalFloatDivTest:testDivDeployed(bytes32,bytes32) (runs: 5096, μ: 3685746, ~: 3685875) +DecimalFloatEqTest:testEqDeployed(bytes32,bytes32) (runs: 5096, μ: 3678981, ~: 3678905) +DecimalFloatFloorTest:testFloorDeployed(bytes32) (runs: 5096, μ: 3678442, ~: 3678248) +DecimalFloatFormatTest:testFormatConstants() (gas: 3678706) +DecimalFloatFormatTest:testFormatDeployed(bytes32,bytes32,bytes32) (runs: 2549, μ: 3704560, ~: 3710763) +DecimalFloatFracTest:testFracDeployed(bytes32) (runs: 5096, μ: 3678812, ~: 3678775) +DecimalFloatFromFixedDecimalLosslessTest:testFromFixedDecimalLosslessDeployed(uint256,uint8) (runs: 5096, μ: 3679399, ~: 3679343) +DecimalFloatFromFixedDecimalLossyTest:testFromFixedDecimalLossyDeployed(uint256,uint8) (runs: 5096, μ: 3679904, ~: 3679832) +DecimalFloatGtTest:testGtDeployed(bytes32,bytes32) (runs: 5096, μ: 3678878, ~: 3678802) +DecimalFloatGteTest:testGteDeployed(bytes32,bytes32) (runs: 5096, μ: 3678952, ~: 3678875) +DecimalFloatInvTest:testInvDeployed(bytes32) (runs: 5096, μ: 3684106, ~: 3684163) +DecimalFloatIsZeroTest:testIsZeroDeployed(bytes32) (runs: 5096, μ: 3678112, ~: 3678112) +DecimalFloatLtTest:testLtDeployed(bytes32,bytes32) (runs: 5096, μ: 3678855, ~: 3678779) +DecimalFloatLteTest:testLteDeployed(bytes32,bytes32) (runs: 5096, μ: 3678909, ~: 3678832) +DecimalFloatMaxTest:testMaxDeployed(bytes32,bytes32) (runs: 5096, μ: 3678962, ~: 3678898) +DecimalFloatMinTest:testMinDeployed(bytes32,bytes32) (runs: 5096, μ: 3678937, ~: 3678874) +DecimalFloatMinusTest:testMinusDeployed(bytes32) (runs: 5096, μ: 3678732, ~: 3678733) +DecimalFloatMulTest:testMulDeployed(bytes32,bytes32) (runs: 5096, μ: 3682723, ~: 3683509) DecimalFloatPackLosslessTest:testPackDeployed(int224,int32) (runs: 5096, μ: 170044, ~: 170045) -DecimalFloatParseTest:testParseDeployed(string) (runs: 5096, μ: 3614363, ~: 3614232) -DecimalFloatPowTest:testPowDeployed(bytes32,bytes32) (runs: 5096, μ: 3663866, ~: 3641157) -DecimalFloatSqrtTest:testSqrtDeployed(bytes32) (runs: 5096, μ: 3628101, ~: 3629825) -DecimalFloatSubTest:testSubDeployed(bytes32,bytes32) (runs: 5096, μ: 3617533, ~: 3617568) -DecimalFloatToFixedDecimalLosslessTest:testToFixedDecimalLosslessDeployed(bytes32,uint8) (runs: 5096, μ: 3613070, ~: 3612964) -DecimalFloatToFixedDecimalLossyTest:testToFixedDecimalLossyDeployed(bytes32,uint8) (runs: 5096, μ: 3613141, ~: 3613448) +DecimalFloatParseTest:testParseDeployed(string) (runs: 5096, μ: 3681394, ~: 3681262) +DecimalFloatPowTest:testPowDeployed(bytes32,bytes32) (runs: 5096, μ: 3727410, ~: 3708099) +DecimalFloatSqrtTest:testSqrtDeployed(bytes32) (runs: 5096, μ: 3695326, ~: 3696789) +DecimalFloatSubTest:testSubDeployed(bytes32,bytes32) (runs: 5096, μ: 3684442, ~: 3684518) +DecimalFloatToFixedDecimalLosslessTest:testToFixedDecimalLosslessDeployed(bytes32,uint8) (runs: 5096, μ: 3680040, ~: 3679950) +DecimalFloatToFixedDecimalLossyTest:testToFixedDecimalLossyDeployed(bytes32,uint8) (runs: 5096, μ: 3680113, ~: 3680412) LibDecimalFloatAbsTest:testAbsMinValue(int32) (runs: 5038, μ: 5162, ~: 5162) LibDecimalFloatAbsTest:testAbsNegative(int256,int32) (runs: 5096, μ: 10536, ~: 10754) LibDecimalFloatAbsTest:testAbsNonNegative(int256,int32) (runs: 5096, μ: 9700, ~: 9316) @@ -306,11 +307,10 @@ LibDecimalFloatSubTest:testSubPacked(bytes32,bytes32) (runs: 5096, μ: 12228, ~: LibFormatDecimalFloatCountSigFigs:testCountSigFigsExamples() (gas: 83596) LibFormatDecimalFloatCountSigFigs:testCountSigFigsOne(int256) (runs: 5096, μ: 31877, ~: 31719) LibFormatDecimalFloatCountSigFigs:testCountSigFigsZero(int256) (runs: 5096, μ: 3764, ~: 3764) -LibFormatDecimalFloatToDecimalStringTest:testFormatDecimalCustomSigFigs() (gas: 26551) -LibFormatDecimalFloatToDecimalStringTest:testFormatDecimalExamples() (gas: 964702) -LibFormatDecimalFloatToDecimalStringTest:testFormatDecimalRoundTripExamples() (gas: 482274) -LibFormatDecimalFloatToDecimalStringTest:testFormatDecimalRoundTripNegative(int256,uint256) (runs: 5096, μ: 71852, ~: 74944) -LibFormatDecimalFloatToDecimalStringTest:testFormatDecimalRoundTripNonNegative(uint256,uint256) (runs: 5096, μ: 55582, ~: 47513) +LibFormatDecimalFloatToDecimalStringTest:testFormatDecimalExamples() (gas: 792935) +LibFormatDecimalFloatToDecimalStringTest:testFormatDecimalRoundTripExamples() (gas: 793404) +LibFormatDecimalFloatToDecimalStringTest:testFormatDecimalRoundTripNegative(int256,bool) (runs: 5096, μ: 66724, ~: 65001) +LibFormatDecimalFloatToDecimalStringTest:testFormatDecimalRoundTripNonNegative(uint256,bool) (runs: 5096, μ: 49144, ~: 50845) LibLogTableBytesTest:testToBytesAntiLogTableDec() (gas: 158047) LibLogTableBytesTest:testToBytesAntiLogTableDecSmall() (gas: 160663) LibLogTableBytesTest:testToBytesLogTableDec() (gas: 141790) diff --git a/crates/float/src/js_api.rs b/crates/float/src/js_api.rs index e4bb5618..a206839b 100644 --- a/crates/float/src/js_api.rs +++ b/crates/float/src/js_api.rs @@ -401,7 +401,51 @@ impl Float { Self::zero() } - /// Formats the float as a decimal string. + /// Returns the default minimum value for scientific notation formatting (1e-4). + /// + /// # Returns + /// + /// * `Ok(Float)` - The default minimum (1e-4). + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ```typescript + /// const minResult = Float.formatDefaultScientificMin(); + /// if (minResult.error) { + /// console.error(minResult.error); + /// } + /// const min = minResult.value; + /// assert(min.format().value === "0.0001"); + /// ``` + #[wasm_export(js_name = "formatDefaultScientificMin", preserve_js_class)] + pub fn format_default_scientific_min_js() -> Result { + Self::format_default_scientific_min() + } + + /// Returns the default maximum value for scientific notation formatting (1e9). + /// + /// # Returns + /// + /// * `Ok(Float)` - The default maximum (1e9). + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ```typescript + /// const maxResult = Float.formatDefaultScientificMax(); + /// if (maxResult.error) { + /// console.error(maxResult.error); + /// } + /// const max = maxResult.value; + /// assert(max.format().value === "1000000000"); + /// ``` + #[wasm_export(js_name = "formatDefaultScientificMax", preserve_js_class)] + pub fn format_default_scientific_max_js() -> Result { + Self::format_default_scientific_max() + } + + /// Formats the float as a decimal string using default scientific notation range (1e-4 to 1e9). /// /// # Returns /// @@ -416,18 +460,19 @@ impl Float { /// console.error(floatResult.error); /// } /// const float = floatResult.value; - /// assert(float.format() === "2.5"); + /// const formatResult = float.format(); + /// assert(formatResult.value === "2.5"); /// ``` #[wasm_export(js_name = "format")] pub fn format_js(&self) -> Result { self.format() } - /// Formats the float as a decimal string with a specified significant figures limit. + /// Formats the float as a decimal string with explicit scientific notation control. /// /// # Arguments /// - /// * `sig_figs_limit` - The significant figures limit. + /// * `scientific` - If true, always use scientific notation. If false, use decimal notation. /// /// # Returns /// @@ -437,16 +482,47 @@ impl Float { /// # Example /// /// ```typescript - /// const floatResult = Float.parse("3.14159265359"); - /// if (floatResult.error) { - /// console.error(floatResult.error); - /// } - /// const float = floatResult.value; - /// assert(float.formatWithLimit(5) === "3.1416"); + /// const float = Float.parse("123.456").value!; + /// + /// const decResult = float.formatWithScientific(false); + /// assert(decResult.value === "123.456"); + /// + /// const sciResult = float.formatWithScientific(true); + /// assert(sciResult.value === "1.23456e2"); + /// ``` + #[wasm_export(js_name = "formatWithScientific")] + pub fn format_with_scientific_js(&self, scientific: bool) -> Result { + self.format_with_scientific(scientific) + } + + /// Formats the float as a decimal string with a custom scientific notation range. + /// + /// # Arguments + /// + /// * `scientific_min` - Values smaller than this (in absolute value) use scientific notation. + /// * `scientific_max` - Values larger than this (in absolute value) use scientific notation. + /// + /// # Returns + /// + /// * `Ok(String)` - The formatted string. + /// * `Err(FloatError)` - If formatting fails. + /// + /// # Example + /// + /// ```typescript + /// const float = Float.parse("0.5").value!; + /// const min = Float.parse("1").value!; + /// const max = Float.parse("100").value!; + /// const result = float.formatWithRange(min, max); + /// assert(result.value === "5e-1"); /// ``` - #[wasm_export(js_name = "formatWithLimit")] - pub fn format_with_limit_js(&self, sig_figs_limit: u32) -> Result { - self.format_with_limit(sig_figs_limit) + #[wasm_export(js_name = "formatWithRange")] + pub fn format_with_range_js( + &self, + scientific_min: &Self, + scientific_max: &Self, + ) -> Result { + self.format_with_range(*scientific_min, *scientific_max) } /// Returns `true` if `self` is less than `b`. diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 56ca2d66..50becfe5 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -496,32 +496,124 @@ impl Float { }) } - /// Formats the float as a decimal string with a default significant figures limit of 18. + /// Returns the default minimum value for scientific notation formatting (1e-4). + /// + /// Values smaller than this (in absolute value) will be formatted in scientific notation. + /// + /// # Returns + /// + /// * `Ok(Float)` - The default minimum (1e-4). + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ``` + /// use rain_math_float::Float; + /// + /// let min = Float::format_default_scientific_min()?; + /// assert_eq!(min.format()?, "0.0001"); + /// + /// anyhow::Ok(()) + /// ``` + pub fn format_default_scientific_min() -> Result { + let calldata = DecimalFloat::FORMAT_DEFAULT_SCIENTIFIC_MINCall {}.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::FORMAT_DEFAULT_SCIENTIFIC_MINCall::abi_decode_returns( + output.as_ref(), + )?; + Ok(Float(decoded)) + }) + } + + /// Returns the default maximum value for scientific notation formatting (1e9). + /// + /// Values larger than this (in absolute value) will be formatted in scientific notation. + /// + /// # Returns + /// + /// * `Ok(Float)` - The default maximum (1e9). + /// * `Err(FloatError)` - If the EVM call fails. + /// + /// # Example + /// + /// ``` + /// use rain_math_float::Float; + /// + /// let max = Float::format_default_scientific_max()?; + /// assert_eq!(max.format()?, "1000000000"); + /// + /// anyhow::Ok(()) + /// ``` + pub fn format_default_scientific_max() -> Result { + let calldata = DecimalFloat::FORMAT_DEFAULT_SCIENTIFIC_MAXCall {}.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::FORMAT_DEFAULT_SCIENTIFIC_MAXCall::abi_decode_returns( + output.as_ref(), + )?; + Ok(Float(decoded)) + }) + } + + /// Formats the float as a decimal string using default scientific notation range (1e-4 to 1e9). + /// + /// Values within the range [1e-4, 1e9] will use decimal notation. + /// Values outside this range will use scientific notation. /// /// # Returns /// /// * `Ok(String)` - The formatted string. /// * `Err(FloatError)` - If formatting fails. /// - /// # Example + /// # Examples /// + /// Values within the default range use decimal notation: /// ``` /// use rain_math_float::Float; /// - /// let float = Float::parse("2.5".to_string())?; - /// assert_eq!(float.format()?, "2.5"); + /// // At the boundaries (inclusive) + /// assert_eq!(Float::parse("0.0001".to_string())?.format()?, "0.0001"); // 1e-4 + /// assert_eq!(Float::parse("1000000000".to_string())?.format()?, "1000000000"); // 1e9 + /// + /// // Within range + /// assert_eq!(Float::parse("2.5".to_string())?.format()?, "2.5"); + /// assert_eq!(Float::parse("123.456".to_string())?.format()?, "123.456"); + /// assert_eq!(Float::parse("0.001".to_string())?.format()?, "0.001"); + /// assert_eq!(Float::parse("1000000".to_string())?.format()?, "1000000"); + /// + /// anyhow::Ok(()) + /// ``` + /// + /// Values outside the default range use scientific notation: + /// ``` + /// use rain_math_float::Float; + /// + /// // Smaller than 1e-4 + /// assert_eq!(Float::parse("0.00001".to_string())?.format()?, "1e-5"); + /// assert_eq!(Float::parse("0.000001".to_string())?.format()?, "1e-6"); + /// + /// // Larger than 1e9 + /// assert_eq!(Float::parse("10000000000".to_string())?.format()?, "1e10"); + /// assert_eq!(Float::parse("123000000000".to_string())?.format()?, "1.23e11"); /// /// anyhow::Ok(()) /// ``` pub fn format(self) -> Result { - self.format_with_limit(18) + let Float(a) = self; + let calldata = DecimalFloat::format_1Call { a }.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::format_1Call::abi_decode_returns(output.as_ref())?; + Ok(decoded) + }) } - /// Formats the float as a decimal string with a specified significant figures limit. + /// Formats the float as a decimal string with explicit scientific notation control. /// /// # Arguments /// - /// * `sig_figs_limit` - The significant figures limit. + /// * `scientific` - If true, always use scientific notation. If false, use decimal notation. /// /// # Returns /// @@ -534,20 +626,62 @@ impl Float { /// use rain_math_float::Float; /// /// let float = Float::parse("3.14".to_string())?; - /// assert_eq!(float.format_with_limit(5)?, "3.14"); + /// assert_eq!(float.format_with_scientific(false)?, "3.14"); + /// assert_eq!(float.format_with_scientific(true)?, "3.14"); /// /// anyhow::Ok(()) /// ``` - pub fn format_with_limit(self, sig_figs_limit: u32) -> Result { + pub fn format_with_scientific(self, scientific: bool) -> Result { let Float(a) = self; - let calldata = DecimalFloat::formatCall { + let calldata = DecimalFloat::format_0Call { a, scientific }.abi_encode(); + + execute_call(Bytes::from(calldata), |output| { + let decoded = DecimalFloat::format_0Call::abi_decode_returns(output.as_ref())?; + Ok(decoded) + }) + } + + /// Formats the float as a decimal string with a custom scientific notation range. + /// + /// # Arguments + /// + /// * `scientific_min` - Values smaller than this (in absolute value) use scientific notation. + /// * `scientific_max` - Values larger than this (in absolute value) use scientific notation. + /// + /// # Returns + /// + /// * `Ok(String)` - The formatted string. + /// * `Err(FloatError)` - If formatting fails. + /// + /// # Example + /// + /// ``` + /// use rain_math_float::Float; + /// + /// let float = Float::parse("0.001".to_string())?; + /// let min = Float::parse("0.01".to_string())?; + /// let max = Float::parse("100".to_string())?; + /// assert_eq!(float.format_with_range(min, max)?, "1e-3"); + /// + /// anyhow::Ok(()) + /// ``` + pub fn format_with_range( + self, + scientific_min: Self, + scientific_max: Self, + ) -> Result { + let Float(a) = self; + let Float(scientific_min_inner) = scientific_min; + let Float(scientific_max_inner) = scientific_max; + let calldata = DecimalFloat::format_2Call { a, - sigFigsLimit: U256::from(sig_figs_limit), + scientificMin: scientific_min_inner, + scientificMax: scientific_max_inner, } .abi_encode(); execute_call(Bytes::from(calldata), |output| { - let decoded = DecimalFloat::formatCall::abi_decode_returns(output.as_ref())?; + let decoded = DecimalFloat::format_2Call::abi_decode_returns(output.as_ref())?; Ok(decoded) }) } @@ -1432,13 +1566,9 @@ mod tests { fn test_minus_format() { let float = Float::parse("-123.1234234625468391".to_string()).unwrap(); let negated = float.neg().unwrap(); - let formatted = negated.format().unwrap(); - assert_eq!(formatted, "1.231234234625468391e2"); - let float = Float::parse(formatted).unwrap(); - let negated = float.neg().unwrap(); - let formatted = negated.format().unwrap(); - assert_eq!(formatted, "-1.231234234625468391e2"); + let formatted_decimal = negated.format_with_scientific(false).unwrap(); + assert_eq!(formatted_decimal, "123.1234234625468391"); let float = Float::parse("0".to_string()).unwrap(); let negated = float.neg().unwrap(); diff --git a/src/concrete/DecimalFloat.sol b/src/concrete/DecimalFloat.sol index aed1d7ef..23ca5438 100644 --- a/src/concrete/DecimalFloat.sol +++ b/src/concrete/DecimalFloat.sol @@ -9,6 +9,16 @@ import {LibParseDecimalFloat} from "../lib/parse/LibParseDecimalFloat.sol"; contract DecimalFloat { using LibDecimalFloat for Float; + /// The default minimum value for scientific formatting. 1e-4 + // slither-disable-next-line too-many-digits + Float public constant FORMAT_DEFAULT_SCIENTIFIC_MIN = + Float.wrap(0xfffffffc00000000000000000000000000000000000000000000000000000001); + + /// The default maximum value for scientific formatting. 1e9 + // slither-disable-next-line too-many-digits + Float public constant FORMAT_DEFAULT_SCIENTIFIC_MAX = + Float.wrap(0x0000000900000000000000000000000000000000000000000000000000000001); + /// Exposes `LibDecimalFloat.FLOAT_MAX_POSITIVE_VALUE` for offchain use. /// @return The maximum positive value of a Float. function maxPositiveValue() external pure returns (Float) { @@ -58,10 +68,31 @@ contract DecimalFloat { /// Exposes `LibFormatDecimalFloat.toDecimalString` for offchain use. /// @param a The float to format. - /// @param sigFigsLimit The significant figures limit. + /// @param scientificMin The smallest number that won't be formatted in + /// scientific notation. + /// @param scientificMax The largest number that won't be formatted in + /// scientific notation. + /// @return The string representation of the float. + function format(Float a, Float scientificMin, Float scientificMax) public pure returns (string memory) { + require(scientificMin.lt(scientificMax), "scientificMin must be less than scientificMax"); + return LibFormatDecimalFloat.toDecimalString(a, a.lt(scientificMin) || a.gt(scientificMax)); + } + + /// Exposes `LibFormatDecimalFloat.toDecimalString` for offchain use. + /// provides raw bool interface for custom scientific formatting. + /// @param a The float to format. + /// @param scientific Whether to format the float in scientific notation. + /// @return The string representation of the float. + function format(Float a, bool scientific) external pure returns (string memory) { + return LibFormatDecimalFloat.toDecimalString(a, scientific); + } + + /// Exposes `format(Float, Float, Float)` for offchain use. + /// Provides default scientific formatting. + /// @param a The float to format. /// @return The string representation of the float. - function format(Float a, uint256 sigFigsLimit) external pure returns (string memory) { - return LibFormatDecimalFloat.toDecimalString(a, sigFigsLimit); + function format(Float a) external pure returns (string memory) { + return format(a, FORMAT_DEFAULT_SCIENTIFIC_MIN, FORMAT_DEFAULT_SCIENTIFIC_MAX); } /// Exposes `LibDecimalFloat.add` for offchain use. diff --git a/src/lib/format/LibFormatDecimalFloat.sol b/src/lib/format/LibFormatDecimalFloat.sol index 9712cec7..55933941 100644 --- a/src/lib/format/LibFormatDecimalFloat.sol +++ b/src/lib/format/LibFormatDecimalFloat.sol @@ -51,14 +51,12 @@ library LibFormatDecimalFloat { /// @param float The decimal float to format. /// @return The string representation of the decimal float. //slither-disable-next-line cyclomatic-complexity - function toDecimalString(Float float, uint256 sigFigsLimit) internal pure returns (string memory) { + function toDecimalString(Float float, bool scientific) internal pure returns (string memory) { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.unpack(float); if (signedCoefficient == 0) { return "0"; } - uint256 sigFigs = countSigFigs(signedCoefficient, exponent); - bool scientific = sigFigs > sigFigsLimit; uint256 scaleExponent; uint256 scale = 0; if (scientific) { diff --git a/test/src/concrete/DecimalFloat.format.t.sol b/test/src/concrete/DecimalFloat.format.t.sol index 5251837d..ff47c6e3 100644 --- a/test/src/concrete/DecimalFloat.format.t.sol +++ b/test/src/concrete/DecimalFloat.format.t.sol @@ -10,20 +10,33 @@ import {LibFormatDecimalFloat} from "src/lib/format/LibFormatDecimalFloat.sol"; contract DecimalFloatFormatTest is Test { using LibDecimalFloat for Float; - function formatExternal(Float a, uint256 sigFigsLimit) external pure returns (string memory) { - return LibFormatDecimalFloat.toDecimalString(a, sigFigsLimit); + function formatExternal(Float a, Float scientificMin, Float scientificMax) external pure returns (string memory) { + return LibFormatDecimalFloat.toDecimalString(a, a.lt(scientificMin) || a.gt(scientificMax)); } - function testFormatDeployed(Float a, uint256 sigFigsLimit) external { + function testFormatDeployed(Float a, Float scientificMin, Float scientificMax) external { + vm.assume(scientificMin.lt(scientificMax)); + DecimalFloat deployed = new DecimalFloat(); - try this.formatExternal(a, sigFigsLimit) returns (string memory str) { - string memory deployedStr = deployed.format(a, sigFigsLimit); + try this.formatExternal(a, scientificMin, scientificMax) returns (string memory str) { + string memory deployedStr = deployed.format(a, scientificMin, scientificMax); assertEq(str, deployedStr); } catch (bytes memory err) { vm.expectRevert(err); - deployed.format(a, sigFigsLimit); + deployed.format(a, scientificMin, scientificMax); } } + + function testFormatConstants() external { + DecimalFloat deployed = new DecimalFloat(); + + assertEq( + Float.unwrap(deployed.FORMAT_DEFAULT_SCIENTIFIC_MIN()), Float.unwrap(LibDecimalFloat.packLossless(1, -4)) + ); + assertEq( + Float.unwrap(deployed.FORMAT_DEFAULT_SCIENTIFIC_MAX()), Float.unwrap(LibDecimalFloat.packLossless(1, 9)) + ); + } } diff --git a/test/src/lib/format/LibFormatDecimalFloat.toDecimalString.t.sol b/test/src/lib/format/LibFormatDecimalFloat.toDecimalString.t.sol index 9b63374d..35eb732a 100644 --- a/test/src/lib/format/LibFormatDecimalFloat.toDecimalString.t.sol +++ b/test/src/lib/format/LibFormatDecimalFloat.toDecimalString.t.sol @@ -14,84 +14,104 @@ contract LibFormatDecimalFloatToDecimalStringTest is Test { using LibDecimalFloat for Float; using LibFormatDecimalFloat for Float; - function checkFormat(int256 signedCoefficient, int256 exponent, uint256 sigFigsLimit, string memory expected) + function checkFormat(int256 signedCoefficient, int256 exponent, bool scientific, string memory expected) internal pure { - string memory actual = LibFormatDecimalFloat.toDecimalString( - LibDecimalFloat.packLossless(signedCoefficient, exponent), sigFigsLimit - ); + string memory actual = + LibFormatDecimalFloat.toDecimalString(LibDecimalFloat.packLossless(signedCoefficient, exponent), scientific); assertEq(actual, expected, "Formatted value mismatch"); } - function checkRoundFromString(string memory s, Float expected) internal pure { + function checkRoundFromString(string memory s, Float expected, bool scientific) internal pure { (bytes4 err, Float parsed) = LibParseDecimalFloat.parseDecimalFloat(s); assertEq(err, 0, "Parse error"); assertTrue(expected.eq(parsed), "Round trip failed"); // Canonicalization: format(parse(s)) == s - string memory reFormatted = LibFormatDecimalFloat.toDecimalString(parsed, 9); + string memory reFormatted = LibFormatDecimalFloat.toDecimalString(parsed, scientific); assertEq(s, reFormatted, "Formatting not canonical"); } /// Test round tripping examples. function testFormatDecimalRoundTripExamples() external pure { checkRoundFromString( - "1.2345678901234567890123456789e29", LibDecimalFloat.packLossless(123456789012345678901234567890, 0) + "1.2345678901234567890123456789e29", LibDecimalFloat.packLossless(123456789012345678901234567890, 0), true ); - checkRoundFromString("0", LibDecimalFloat.packLossless(0, 0)); + checkRoundFromString("0", LibDecimalFloat.packLossless(0, 0), true); + checkRoundFromString("0", LibDecimalFloat.packLossless(0, 0), false); checkRoundFromString( - "-1.2345678901234567890123456789e29", LibDecimalFloat.packLossless(-123456789012345678901234567890, 0) + "-1.2345678901234567890123456789e29", LibDecimalFloat.packLossless(-123456789012345678901234567890, 0), true ); - checkRoundFromString("1", LibDecimalFloat.packLossless(1, 0)); - checkRoundFromString("-1", LibDecimalFloat.packLossless(-1, 0)); - checkRoundFromString("100", LibDecimalFloat.packLossless(100, 0)); - checkRoundFromString("-100", LibDecimalFloat.packLossless(-100, 0)); - checkRoundFromString("0.01", LibDecimalFloat.packLossless(1, -2)); - checkRoundFromString("-0.01", LibDecimalFloat.packLossless(-1, -2)); - checkRoundFromString("0.1", LibDecimalFloat.packLossless(1, -1)); - checkRoundFromString("-0.1", LibDecimalFloat.packLossless(-1, -1)); - checkRoundFromString("0.101", LibDecimalFloat.packLossless(101, -3)); - checkRoundFromString("-0.101", LibDecimalFloat.packLossless(-101, -3)); - checkRoundFromString("1.1", LibDecimalFloat.packLossless(11, -1)); - checkRoundFromString("-1.1", LibDecimalFloat.packLossless(-11, -1)); - checkRoundFromString("123456789", LibDecimalFloat.packLossless(123456789, 0)); - checkRoundFromString("-123456789", LibDecimalFloat.packLossless(-123456789, 0)); - checkRoundFromString("1.23456789e9", LibDecimalFloat.packLossless(1234567890, 0)); - checkRoundFromString("-1.23456789e9", LibDecimalFloat.packLossless(-1234567890, 0)); - checkRoundFromString("1.019001501928e-6", LibDecimalFloat.packLossless(1019001501928, -18)); - checkRoundFromString("-1.019001501928e-6", LibDecimalFloat.packLossless(-1019001501928, -18)); - checkRoundFromString("1e9", LibDecimalFloat.packLossless(1000000000, 0)); - checkRoundFromString("-1e9", LibDecimalFloat.packLossless(-1000000000, 0)); - checkRoundFromString("1e-76", LibDecimalFloat.packLossless(1, -76)); - checkRoundFromString("-1e-76", LibDecimalFloat.packLossless(-1, -76)); - checkRoundFromString("1e76", LibDecimalFloat.packLossless(1, 76)); - checkRoundFromString("-1e76", LibDecimalFloat.packLossless(-1, 76)); - checkRoundFromString("1e200", LibDecimalFloat.packLossless(1, 200)); - checkRoundFromString("-1e200", LibDecimalFloat.packLossless(-1, 200)); + checkRoundFromString("1", LibDecimalFloat.packLossless(1, 0), false); + checkRoundFromString("1", LibDecimalFloat.packLossless(1, 0), true); + checkRoundFromString("-1", LibDecimalFloat.packLossless(-1, 0), false); + checkRoundFromString("-1", LibDecimalFloat.packLossless(-1, 0), true); + checkRoundFromString("100", LibDecimalFloat.packLossless(100, 0), false); + checkRoundFromString("1e2", LibDecimalFloat.packLossless(100, 0), true); + checkRoundFromString("-100", LibDecimalFloat.packLossless(-100, 0), false); + checkRoundFromString("-1e2", LibDecimalFloat.packLossless(-100, 0), true); + checkRoundFromString("0.01", LibDecimalFloat.packLossless(1, -2), false); + checkRoundFromString("1e-2", LibDecimalFloat.packLossless(1, -2), true); + checkRoundFromString("-0.01", LibDecimalFloat.packLossless(-1, -2), false); + checkRoundFromString("-1e-2", LibDecimalFloat.packLossless(-1, -2), true); + checkRoundFromString("0.1", LibDecimalFloat.packLossless(1, -1), false); + checkRoundFromString("1e-1", LibDecimalFloat.packLossless(1, -1), true); + checkRoundFromString("-1e-1", LibDecimalFloat.packLossless(-1, -1), true); + checkRoundFromString("-0.1", LibDecimalFloat.packLossless(-1, -1), false); + checkRoundFromString("0.101", LibDecimalFloat.packLossless(101, -3), false); + checkRoundFromString("1.01e-1", LibDecimalFloat.packLossless(101, -3), true); + checkRoundFromString("-0.101", LibDecimalFloat.packLossless(-101, -3), false); + checkRoundFromString("-1.01e-1", LibDecimalFloat.packLossless(-101, -3), true); + checkRoundFromString("1.1", LibDecimalFloat.packLossless(11, -1), false); + checkRoundFromString("1.1", LibDecimalFloat.packLossless(11, -1), true); + checkRoundFromString("-1.1", LibDecimalFloat.packLossless(-11, -1), false); + checkRoundFromString("-1.1", LibDecimalFloat.packLossless(-11, -1), true); + checkRoundFromString("123456789", LibDecimalFloat.packLossless(123456789, 0), false); + checkRoundFromString("1.23456789e8", LibDecimalFloat.packLossless(123456789, 0), true); + checkRoundFromString("-123456789", LibDecimalFloat.packLossless(-123456789, 0), false); + checkRoundFromString("-1.23456789e8", LibDecimalFloat.packLossless(-123456789, 0), true); + checkRoundFromString("1.23456789e9", LibDecimalFloat.packLossless(1234567890, 0), true); + checkRoundFromString("123456789", LibDecimalFloat.packLossless(123456789, 0), false); + checkRoundFromString("-1.23456789e9", LibDecimalFloat.packLossless(-1234567890, 0), true); + checkRoundFromString("-123456789", LibDecimalFloat.packLossless(-123456789, 0), false); + checkRoundFromString("1.019001501928e-6", LibDecimalFloat.packLossless(1019001501928, -18), true); + checkRoundFromString("0.000001019001501928", LibDecimalFloat.packLossless(1019001501928, -18), false); + checkRoundFromString("-1.019001501928e-6", LibDecimalFloat.packLossless(-1019001501928, -18), true); + checkRoundFromString("-0.000001019001501928", LibDecimalFloat.packLossless(-1019001501928, -18), false); + checkRoundFromString("1e9", LibDecimalFloat.packLossless(1000000000, 0), true); + checkRoundFromString("1000000000", LibDecimalFloat.packLossless(1000000000, 0), false); + checkRoundFromString("-1e9", LibDecimalFloat.packLossless(-1000000000, 0), true); + checkRoundFromString("-1000000000", LibDecimalFloat.packLossless(-1000000000, 0), false); + checkRoundFromString("1e-76", LibDecimalFloat.packLossless(1, -76), true); + checkRoundFromString("-1e-76", LibDecimalFloat.packLossless(-1, -76), true); + checkRoundFromString("1e76", LibDecimalFloat.packLossless(1, 76), true); + checkRoundFromString("-1e76", LibDecimalFloat.packLossless(-1, 76), true); + checkRoundFromString("1e200", LibDecimalFloat.packLossless(1, 200), true); + checkRoundFromString("-1e200", LibDecimalFloat.packLossless(-1, 200), true); } /// Test round tripping a value through parse and format. - function testFormatDecimalRoundTripNonNegative(uint256 value, uint256 sigFigsLimit) external pure { + function testFormatDecimalRoundTripNonNegative(uint256 value, bool scientific) external pure { value = bound(value, 0, uint256(int256(type(int224).max))); Float float = LibDecimalFloat.fromFixedDecimalLosslessPacked(value, 18); - string memory formatted = LibFormatDecimalFloat.toDecimalString(float, sigFigsLimit); + string memory formatted = LibFormatDecimalFloat.toDecimalString(float, scientific); (bytes4 errorCode, Float parsed) = LibParseDecimalFloat.parseDecimalFloat(formatted); assertEq(errorCode, 0, "Parse error"); assertTrue(float.eq(parsed), "Round trip failed"); // Canonicalization: format(parse(format(x))) == format(x) - string memory reFormatted = LibFormatDecimalFloat.toDecimalString(parsed, sigFigsLimit); + string memory reFormatted = LibFormatDecimalFloat.toDecimalString(parsed, scientific); assertEq(formatted, reFormatted, "Formatting not canonical"); } /// Negative matches positive. - function testFormatDecimalRoundTripNegative(int256 value, uint256 sigFigsLimit) external pure { + function testFormatDecimalRoundTripNegative(int256 value, bool scientific) external pure { value = bound(value, 1, int256(type(int128).max)); // value [1, int256(type(int128).max)] // forge-lint: disable-next-line(unsafe-typecast) Float float = LibDecimalFloat.fromFixedDecimalLosslessPacked(uint256(value), 18); - string memory formatted = float.toDecimalString(sigFigsLimit); + string memory formatted = float.toDecimalString(scientific); float = float.minus(); - string memory formattedNeg = float.toDecimalString(sigFigsLimit); + string memory formattedNeg = float.toDecimalString(scientific); assertEq(string.concat("-", formatted), formattedNeg, "Negative format mismatch"); // Parse/eq for negative path as well @@ -99,142 +119,134 @@ contract LibFormatDecimalFloatToDecimalStringTest is Test { assertEq(err, 0, "Parse error (neg)"); assertTrue(float.eq(parsedNeg), "Round trip failed (neg)"); // Canonicalization for negative: format(parse(s)) == s - string memory reFormattedNeg = LibFormatDecimalFloat.toDecimalString(parsedNeg, sigFigsLimit); + string memory reFormattedNeg = LibFormatDecimalFloat.toDecimalString(parsedNeg, scientific); assertEq(formattedNeg, reFormattedNeg, "Formatting not canonical (neg)"); } /// Test some specific examples. function testFormatDecimalExamples() external pure { // pos decs - checkFormat(123456789012345678901234567890, 0, 9, "1.2345678901234567890123456789e29"); - checkFormat(123456789012345678901234567890, -1, 9, "1.2345678901234567890123456789e28"); - checkFormat(123456789012345678901234567890, -2, 9, "1.2345678901234567890123456789e27"); - checkFormat(123456789012345678901234567890, -3, 9, "1.2345678901234567890123456789e26"); - checkFormat(123456789012345678901234567890, -4, 9, "1.2345678901234567890123456789e25"); - checkFormat(123456789012345678901234567890, -5, 9, "1.2345678901234567890123456789e24"); - checkFormat(123456789012345678901234567890, -6, 9, "1.2345678901234567890123456789e23"); + checkFormat(123456789012345678901234567890, 0, true, "1.2345678901234567890123456789e29"); + checkFormat(123456789012345678901234567890, -1, true, "1.2345678901234567890123456789e28"); + checkFormat(123456789012345678901234567890, -2, true, "1.2345678901234567890123456789e27"); + checkFormat(123456789012345678901234567890, -3, true, "1.2345678901234567890123456789e26"); + checkFormat(123456789012345678901234567890, -4, true, "1.2345678901234567890123456789e25"); + checkFormat(123456789012345678901234567890, -5, true, "1.2345678901234567890123456789e24"); + checkFormat(123456789012345678901234567890, -6, true, "1.2345678901234567890123456789e23"); // zeros - checkFormat(0, 0, 9, "0"); - checkFormat(0, -1, 9, "0"); - checkFormat(0, -2, 9, "0"); - checkFormat(0, -3, 9, "0"); - checkFormat(0, 1, 9, "0"); - checkFormat(0, 2, 9, "0"); - checkFormat(0, 3, 9, "0"); + checkFormat(0, 0, true, "0"); + checkFormat(0, -1, true, "0"); + checkFormat(0, -2, true, "0"); + checkFormat(0, -3, true, "0"); + checkFormat(0, 1, true, "0"); + checkFormat(0, 2, true, "0"); + checkFormat(0, 3, true, "0"); // neg decs - checkFormat(-123456789012345678901234567890, 0, 9, "-1.2345678901234567890123456789e29"); - checkFormat(-123456789012345678901234567890, -1, 9, "-1.2345678901234567890123456789e28"); - checkFormat(-123456789012345678901234567890, -2, 9, "-1.2345678901234567890123456789e27"); - checkFormat(-123456789012345678901234567890, -3, 9, "-1.2345678901234567890123456789e26"); - checkFormat(-123456789012345678901234567890, -4, 9, "-1.2345678901234567890123456789e25"); - checkFormat(-123456789012345678901234567890, -5, 9, "-1.2345678901234567890123456789e24"); - checkFormat(-123456789012345678901234567890, -6, 9, "-1.2345678901234567890123456789e23"); + checkFormat(-123456789012345678901234567890, 0, true, "-1.2345678901234567890123456789e29"); + checkFormat(-123456789012345678901234567890, -1, true, "-1.2345678901234567890123456789e28"); + checkFormat(-123456789012345678901234567890, -2, true, "-1.2345678901234567890123456789e27"); + checkFormat(-123456789012345678901234567890, -3, true, "-1.2345678901234567890123456789e26"); + checkFormat(-123456789012345678901234567890, -4, true, "-1.2345678901234567890123456789e25"); + checkFormat(-123456789012345678901234567890, -5, true, "-1.2345678901234567890123456789e24"); + checkFormat(-123456789012345678901234567890, -6, true, "-1.2345678901234567890123456789e23"); // one - checkFormat(1, 0, 9, "1"); + checkFormat(1, 0, true, "1"); // 100 - checkFormat(100, 0, 9, "100"); - checkFormat(10, 1, 9, "100"); - checkFormat(1, 2, 9, "100"); - checkFormat(1000, -1, 9, "100"); + checkFormat(100, 0, false, "100"); + checkFormat(10, 1, false, "100"); + checkFormat(1, 2, false, "100"); + checkFormat(1000, -1, false, "100"); // -100 - checkFormat(-100, 0, 9, "-100"); - checkFormat(-10, 1, 9, "-100"); - checkFormat(-1, 2, 9, "-100"); - checkFormat(-1000, -1, 9, "-100"); + checkFormat(-100, 0, false, "-100"); + checkFormat(-10, 1, false, "-100"); + checkFormat(-1, 2, false, "-100"); + checkFormat(-1000, -1, false, "-100"); // 0.01 - checkFormat(1, -2, 9, "0.01"); - checkFormat(10, -3, 9, "0.01"); - checkFormat(100, -4, 9, "0.01"); - checkFormat(1000, -5, 9, "0.01"); + checkFormat(1, -2, false, "0.01"); + checkFormat(10, -3, false, "0.01"); + checkFormat(100, -4, false, "0.01"); + checkFormat(1000, -5, false, "0.01"); // -0.01 - checkFormat(-1, -2, 9, "-0.01"); - checkFormat(-10, -3, 9, "-0.01"); - checkFormat(-100, -4, 9, "-0.01"); - checkFormat(-1000, -5, 9, "-0.01"); + checkFormat(-1, -2, false, "-0.01"); + checkFormat(-10, -3, false, "-0.01"); + checkFormat(-100, -4, false, "-0.01"); + checkFormat(-1000, -5, false, "-0.01"); // 0.1 - checkFormat(1, -1, 9, "0.1"); - checkFormat(10, -2, 9, "0.1"); - checkFormat(100, -3, 9, "0.1"); - checkFormat(1000, -4, 9, "0.1"); + checkFormat(1, -1, false, "0.1"); + checkFormat(10, -2, false, "0.1"); + checkFormat(100, -3, false, "0.1"); + checkFormat(1000, -4, false, "0.1"); // -0.1 - checkFormat(-1, -1, 9, "-0.1"); - checkFormat(-10, -2, 9, "-0.1"); - checkFormat(-100, -3, 9, "-0.1"); - checkFormat(-1000, -4, 9, "-0.1"); + checkFormat(-1, -1, false, "-0.1"); + checkFormat(-10, -2, false, "-0.1"); + checkFormat(-100, -3, false, "-0.1"); + checkFormat(-1000, -4, false, "-0.1"); // 0.101 - checkFormat(101, -3, 9, "0.101"); - checkFormat(1010, -4, 9, "0.101"); - checkFormat(10100, -5, 9, "0.101"); - checkFormat(101000, -6, 9, "0.101"); + checkFormat(101, -3, false, "0.101"); + checkFormat(1010, -4, false, "0.101"); + checkFormat(10100, -5, false, "0.101"); + checkFormat(101000, -6, false, "0.101"); // -0.101 - checkFormat(-101, -3, 9, "-0.101"); - checkFormat(-1010, -4, 9, "-0.101"); - checkFormat(-10100, -5, 9, "-0.101"); - checkFormat(-101000, -6, 9, "-0.101"); + checkFormat(-101, -3, false, "-0.101"); + checkFormat(-1010, -4, false, "-0.101"); + checkFormat(-10100, -5, false, "-0.101"); + checkFormat(-101000, -6, false, "-0.101"); // 1.1 - checkFormat(11, -1, 9, "1.1"); - checkFormat(110, -2, 9, "1.1"); - checkFormat(1100, -3, 9, "1.1"); - checkFormat(11000, -4, 9, "1.1"); + checkFormat(11, -1, false, "1.1"); + checkFormat(110, -2, false, "1.1"); + checkFormat(1100, -3, false, "1.1"); + checkFormat(11000, -4, false, "1.1"); // -1.1 - checkFormat(-11, -1, 9, "-1.1"); - checkFormat(-110, -2, 9, "-1.1"); - checkFormat(-1100, -3, 9, "-1.1"); - checkFormat(-11000, -4, 9, "-1.1"); + checkFormat(-11, -1, false, "-1.1"); + checkFormat(-110, -2, false, "-1.1"); + checkFormat(-1100, -3, false, "-1.1"); + checkFormat(-11000, -4, false, "-1.1"); // 9 sig figs - checkFormat(123456789, 0, 9, "123456789"); - checkFormat(-123456789, 0, 9, "-123456789"); - checkFormat(123456789, -1, 9, "12345678.9"); - checkFormat(-123456789, -1, 9, "-12345678.9"); - checkFormat(12345678, 1, 9, "123456780"); - checkFormat(-12345678, 1, 9, "-123456780"); + checkFormat(123456789, 0, false, "123456789"); + checkFormat(-123456789, 0, false, "-123456789"); + checkFormat(123456789, -1, false, "12345678.9"); + checkFormat(-123456789, -1, false, "-12345678.9"); + checkFormat(12345678, 1, false, "123456780"); + checkFormat(-12345678, 1, false, "-123456780"); // 10 sig figs - checkFormat(1234567890, 0, 9, "1.23456789e9"); - checkFormat(-1234567890, 0, 9, "-1.23456789e9"); - checkFormat(123456789, 1, 9, "1.23456789e9"); - checkFormat(-123456789, 1, 9, "-1.23456789e9"); - checkFormat(1, -10, 9, "1e-10"); + checkFormat(1234567890, 0, true, "1.23456789e9"); + checkFormat(-1234567890, 0, true, "-1.23456789e9"); + checkFormat(123456789, 1, true, "1.23456789e9"); + checkFormat(-123456789, 1, true, "-1.23456789e9"); + checkFormat(1, -10, true, "1e-10"); // examples from fuzz - checkFormat(1019001501928, -18, 9, "1.019001501928e-6"); - checkFormat(-1019001501928, -18, 9, "-1.019001501928e-6"); + checkFormat(1019001501928, -18, true, "1.019001501928e-6"); + checkFormat(-1019001501928, -18, true, "-1.019001501928e-6"); // pure powers of 10 at the cutoff - checkFormat(1000000000, 0, 9, "1e9"); - checkFormat(-1000000000, 0, 9, "-1e9"); + checkFormat(1000000000, 0, true, "1e9"); + checkFormat(-1000000000, 0, true, "-1e9"); // extreme small/large magnitudes still choose scientific - checkFormat(1, -76, 9, "1e-76"); - checkFormat(-1, -76, 9, "-1e-76"); - checkFormat(1, 76, 9, "1e76"); - checkFormat(-1, 76, 9, "-1e76"); + checkFormat(1, -76, true, "1e-76"); + checkFormat(-1, -76, true, "-1e-76"); + checkFormat(1, 76, true, "1e76"); + checkFormat(-1, 76, true, "-1e76"); // impossible sig figs. - checkFormat(1, 200, 1, "1e200"); + checkFormat(1, 200, true, "1e200"); // we can't actually fit 200 zeros into the binary representation so // even though the threshold is 200 we still use scientific notation. - checkFormat(1, 200, 200, "1e200"); - } - - function testFormatDecimalCustomSigFigs() external pure { - // Force rounding under a tighter sig-figs limit. - Float f = LibDecimalFloat.packLossless(12345678, 0); - string memory s = LibFormatDecimalFloat.toDecimalString(f, 5); - // Verify the explicit limit path (adjust expected if rounding policy differs). - assertEq(s, "1.2345678e7", "Custom sig-figs not applied"); + checkFormat(1, 200, true, "1e200"); } } diff --git a/test_js/float.test.ts b/test_js/float.test.ts index 30306eec..028a19b8 100644 --- a/test_js/float.test.ts +++ b/test_js/float.test.ts @@ -153,7 +153,7 @@ describe('Test Float Bindings', () => { const negOne = Float.parse('-1')?.value!; // Test mathematical properties without exposing binary representation - + // All constants should be distinct expect(maxPos.eq(minPos)?.value!).toBe(false); expect(maxNeg.eq(minNeg)?.value!).toBe(false); @@ -176,5 +176,98 @@ describe('Test Float Bindings', () => { expect(maxNeg.gt(negOne)?.value!).toBe(true); // max negative > -1 expect(minNeg.lt(negOne)?.value!).toBe(true); // min negative < -1 }); + + it('should test format default scientific notation constants', () => { + const minResult = Float.formatDefaultScientificMin(); + const maxResult = Float.formatDefaultScientificMax(); + + expect(minResult.error).toBeUndefined(); + expect(maxResult.error).toBeUndefined(); + + const min = minResult.value!; + const max = maxResult.value!; + + // Verify the values + expect(min.format()?.value!).toBe('0.0001'); // 1e-4 + expect(max.format()?.value!).toBe('1000000000'); // 1e9 + }); + + it('should test default formatting behavior', () => { + // Values within default range (1e-4 to 1e9) should use decimal notation + const small = Float.parse('0.0001')?.value!; + expect(small.format()?.value!).toBe('0.0001'); + + const normal = Float.parse('123.456')?.value!; + expect(normal.format()?.value!).toBe('123.456'); + + const large = Float.parse('1000000000')?.value!; + expect(large.format()?.value!).toBe('1000000000'); + + // Values outside default range should use scientific notation + const tooSmall = Float.parse('0.00001')?.value!; + expect(tooSmall.format()?.value!).toBe('1e-5'); + + const tooLarge = Float.parse('10000000000')?.value!; + expect(tooLarge.format()?.value!).toBe('1e10'); + }); + + it('should test formatWithScientific boolean control', () => { + const float = Float.parse('123.456')?.value!; + + // Explicit decimal notation + const decimal = float.formatWithScientific(false); + expect(decimal.error).toBeUndefined(); + expect(decimal.value!).toBe('123.456'); + + // Explicit scientific notation + const scientific = float.formatWithScientific(true); + expect(scientific.error).toBeUndefined(); + expect(scientific.value!).toBe('1.23456e2'); + + // Test with very small number + const small = Float.parse('0.00001')?.value!; + const smallDecimal = small.formatWithScientific(false); + expect(smallDecimal.value!).toBe('0.00001'); + + const smallScientific = small.formatWithScientific(true); + expect(smallScientific.value!).toBe('1e-5'); + }); + + it('should test formatWithRange custom ranges', () => { + const float = Float.parse('0.5')?.value!; + const min = Float.parse('1')?.value!; + const max = Float.parse('100')?.value!; + + // 0.5 is smaller than min (1), so should use scientific notation + const result = float.formatWithRange(min, max); + expect(result.error).toBeUndefined(); + expect(result.value!).toBe('5e-1'); + + // Value within custom range + const inRange = Float.parse('50')?.value!; + const inRangeResult = inRange.formatWithRange(min, max); + expect(inRangeResult.value!).toBe('50'); + + // Value outside custom range (too large) + const outOfRange = Float.parse('1000')?.value!; + const outOfRangeResult = outOfRange.formatWithRange(min, max); + expect(outOfRangeResult.value!).toBe('1e3'); + }); + + it('should test formatting round-trip with new methods', () => { + const original = Float.parse('0.0001')?.value!; + + // Format and parse back + const formatted = original.format()?.value!; + const parsed = Float.parse(formatted)?.value!; + + expect(original.eq(parsed)?.value!).toBe(true); + + // Test with scientific notation + const scientific = original.formatWithScientific(true)?.value!; + const parsedSci = Float.parse(scientific)?.value!; + + expect(original.eq(parsedSci)?.value!).toBe(true); + }); } });