Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2024 11 02 parse float #22

Merged
merged 6 commits into from
Nov 2, 2024
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
3 changes: 2 additions & 1 deletion src/lib/parse/LibParseDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ library LibParseDecimalFloat {

int256 fracValue = int256(LibParseChar.isMask(cursor, end, CMASK_DECIMAL_POINT));
if (fracValue != 0) {
fracValue = 0;
cursor++;
uint256 fracStart = cursor;
cursor = LibParseChar.skipMask(cursor, end, CMASK_NUMERIC_0_9);
Expand All @@ -82,7 +83,7 @@ library LibParseDecimalFloat {
nonZeroCursor--;
}

{
if (nonZeroCursor != fracStart) {
(bytes4 fracErrorSelector, int256 fracValueTmp) =
LibParseDecimal.unsafeDecimalStringToSignedInt(fracStart, nonZeroCursor);
if (fracErrorSelector != 0) {
Expand Down
343 changes: 343 additions & 0 deletions test/src/lib/parse/LibParseDecimalFloat.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
// SPDX-License-Identifier: CAL
pragma solidity =0.8.25;

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

import {LibParseDecimalFloat} from "src/lib/parse/LibParseDecimalFloat.sol";
import {LibBytes, Pointer} from "rain.solmem/lib/LibBytes.sol";
import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";
import {ParseEmptyDecimalString} from "rain.string/error/ErrParse.sol";
import {MalformedExponentDigits, ParseDecimalPrecisionLoss, MalformedDecimalPoint} from "src/error/ErrParse.sol";

contract LibParseDecimalFloatTest is Test {
using LibBytes for bytes;
using Strings for uint256;

function checkParseDecimalFloat(
string memory data,
int256 expectedSignedCoefficient,
int256 expectedExponent,
uint256 expectedCursorAfter
) internal pure {
uint256 cursor = Pointer.unwrap(bytes(data).dataPointer());
(bytes4 errorSelector, uint256 cursorAfter, int256 signedCoefficient, int256 exponent) =
LibParseDecimalFloat.parseDecimalFloat(cursor, Pointer.unwrap(bytes(data).endDataPointer()));
assertEq(errorSelector, bytes4(0));
assertEq(signedCoefficient, expectedSignedCoefficient);
assertEq(exponent, expectedExponent);
assertEq(cursorAfter - cursor, expectedCursorAfter);
}

function checkParseDecimalFloatFail(string memory data, bytes4 expectedErrorSelector, uint256 expectedCursorAfter)
internal
pure
{
uint256 cursor = Pointer.unwrap(bytes(data).dataPointer());
(bytes4 errorSelector, uint256 cursorAfter,,) =
LibParseDecimalFloat.parseDecimalFloat(cursor, Pointer.unwrap(bytes(data).endDataPointer()));
assertEq(errorSelector, expectedErrorSelector);
assertEq(cursorAfter - cursor, expectedCursorAfter);
}

/// Fuzz and round trip.
function testParseLiteralDecimalFloatFuzz(uint256 value, uint8 leadingZerosCount, bool isNeg) external pure {
value = bound(value, 0, uint256(type(int256).max) + (isNeg ? 1 : 0));
string memory str = value.toString();

string memory leadingZeros = new string(leadingZerosCount);
for (uint8 i = 0; i < leadingZerosCount; i++) {
bytes(leadingZeros)[i] = "0";
}

string memory input = string(abi.encodePacked((isNeg ? "-" : ""), leadingZeros, str));

checkParseDecimalFloat(
input,
isNeg ? (value == (uint256(type(int256).max) + 1) ? type(int256).min : -int256(value)) : int256(value),
0,
bytes(input).length
);
}

/// Check some specific examples.
function testParseLiteralDecimalFloatSpecific() external pure {
checkParseDecimalFloat("0", 0, 0, 1);
checkParseDecimalFloat("1", 1, 0, 1);
checkParseDecimalFloat("10", 10, 0, 2);
checkParseDecimalFloat("100", 100, 0, 3);
checkParseDecimalFloat("1000", 1000, 0, 4);
checkParseDecimalFloat("2", 2, 0, 1);
}

/// Check some specific examples with leading zeros.
function testParseLiteralDecimalFloatLeadingZeros() external pure {
checkParseDecimalFloat("0000", 0, 0, 4);
checkParseDecimalFloat("0001", 1, 0, 4);
checkParseDecimalFloat("0010", 10, 0, 4);
checkParseDecimalFloat("0100", 100, 0, 4);
checkParseDecimalFloat("1000", 1000, 0, 4);
checkParseDecimalFloat("0002", 2, 0, 4);
checkParseDecimalFloat(
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
1,
0,
128
);
}

/// Check some examples of decimals.
function testParseLiteralDecimalFloatDecimals() external pure {
checkParseDecimalFloat("0.1", 1, -1, 3);
checkParseDecimalFloat("0.01", 1, -2, 4);
checkParseDecimalFloat("0.001", 1, -3, 5);
checkParseDecimalFloat("0.0001", 1, -4, 6);
checkParseDecimalFloat("0.00001", 1, -5, 7);
checkParseDecimalFloat("0.000001", 1, -6, 8);
checkParseDecimalFloat("0.0000001", 1, -7, 9);
checkParseDecimalFloat("0.00000001", 1, -8, 10);
checkParseDecimalFloat("0.000000001", 1, -9, 11);
checkParseDecimalFloat("0.0000000001", 1, -10, 12);
checkParseDecimalFloat("0.00000000001", 1, -11, 13);
checkParseDecimalFloat("0.000000000001", 1, -12, 14);
checkParseDecimalFloat("0.0000000000001", 1, -13, 15);
checkParseDecimalFloat("0.00000000000001", 1, -14, 16);
checkParseDecimalFloat("0.000000000000001", 1, -15, 17);
checkParseDecimalFloat("0.0000000000000001", 1, -16, 18);
checkParseDecimalFloat("0.00000000000000001", 1, -17, 19);
checkParseDecimalFloat("0.000000000000000001", 1, -18, 20);
checkParseDecimalFloat("0.0000000000000000001", 1, -19, 21);
checkParseDecimalFloat("0.00000000000000000001", 1, -20, 22);
checkParseDecimalFloat("0.000000000000000000001", 1, -21, 23);
checkParseDecimalFloat("0.0000000000000000000001", 1, -22, 24);
checkParseDecimalFloat(
"0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
1,
-127,
129
);
checkParseDecimalFloat(
"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
1,
-127,
254
);

checkParseDecimalFloat("1.1", 11, -1, 3);
checkParseDecimalFloat("1.01", 101, -2, 4);
checkParseDecimalFloat("1.001", 1001, -3, 5);
checkParseDecimalFloat("1.0001", 10001, -4, 6);
checkParseDecimalFloat("1.0001", 10001, -4, 6);

checkParseDecimalFloat("10.1", 101, -1, 4);
checkParseDecimalFloat("10.01", 1001, -2, 5);
checkParseDecimalFloat("10.001", 10001, -3, 6);
checkParseDecimalFloat("10.0001", 100001, -4, 7);

checkParseDecimalFloat("100.1", 1001, -1, 5);
checkParseDecimalFloat("100.01", 10001, -2, 6);
// some trailing zeros
checkParseDecimalFloat("100.001000", 100001, -3, 10);
checkParseDecimalFloat(
"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100.0001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1000001,
-4,
260
);
}

/// Check some examples of exponents.
function testParseLiteralDecimalFloatExponents() external pure {
checkParseDecimalFloat("0e0", 0, 0, 3);
// A capital E.
checkParseDecimalFloat("0E0", 0, 0, 3);
checkParseDecimalFloat("0e1", 0, 1, 3);
checkParseDecimalFloat("0e2", 0, 2, 3);
checkParseDecimalFloat("0e-1", 0, -1, 4);
checkParseDecimalFloat("0e-2", 0, -2, 4);

checkParseDecimalFloat("1e1", 1, 1, 3);
checkParseDecimalFloat("1e2", 1, 2, 3);
checkParseDecimalFloat("1e3", 1, 3, 3);
checkParseDecimalFloat("1e4", 1, 4, 3);
checkParseDecimalFloat("1e5", 1, 5, 3);
checkParseDecimalFloat("1e6", 1, 6, 3);
checkParseDecimalFloat("1e7", 1, 7, 3);
checkParseDecimalFloat("1e8", 1, 8, 3);
checkParseDecimalFloat("1e9", 1, 9, 3);
checkParseDecimalFloat("1e10", 1, 10, 4);
checkParseDecimalFloat("1e11", 1, 11, 4);
checkParseDecimalFloat("1e12", 1, 12, 4);
checkParseDecimalFloat("1e13", 1, 13, 4);
checkParseDecimalFloat("1e14", 1, 14, 4);
checkParseDecimalFloat("1e15", 1, 15, 4);
checkParseDecimalFloat("1e16", 1, 16, 4);
checkParseDecimalFloat("1e17", 1, 17, 4);
checkParseDecimalFloat("1e18", 1, 18, 4);
checkParseDecimalFloat("1e19", 1, 19, 4);
checkParseDecimalFloat("1e20", 1, 20, 4);
checkParseDecimalFloat("1e21", 1, 21, 4);
checkParseDecimalFloat("1e22", 1, 22, 4);
checkParseDecimalFloat("1e23", 1, 23, 4);
checkParseDecimalFloat("1e24", 1, 24, 4);
checkParseDecimalFloat("1e25", 1, 25, 4);
checkParseDecimalFloat("1e26", 1, 26, 4);
checkParseDecimalFloat("1e260", 1, 260, 5);

checkParseDecimalFloat("1e0", 1, 0, 3);
// A capital E.
checkParseDecimalFloat("1E0", 1, 0, 3);
checkParseDecimalFloat("1e-0", 1, 0, 4);
// A capital E.
checkParseDecimalFloat("1E-0", 1, 0, 4);

checkParseDecimalFloat("1e-1", 1, -1, 4);
checkParseDecimalFloat("1e-2", 1, -2, 4);
checkParseDecimalFloat("1e-3", 1, -3, 4);
checkParseDecimalFloat("1e-4", 1, -4, 4);
checkParseDecimalFloat("1e-5", 1, -5, 4);
checkParseDecimalFloat("1e-6", 1, -6, 4);
checkParseDecimalFloat("1e-7", 1, -7, 4);
checkParseDecimalFloat("1e-8", 1, -8, 4);

checkParseDecimalFloat("1e-9912873918273981273918273918739182", 1, -9912873918273981273918273918739182, 37);
checkParseDecimalFloat("1e9912873918273981273918273918739182", 1, 9912873918273981273918273918739182, 36);
checkParseDecimalFloat(
"1e57896044618658097711785492504343953926634992332820282019728792003956564819967", 1, type(int256).max, 79
);
checkParseDecimalFloat(
"57896044618658097711785492504343953926634992332820282019728792003956564819967e57896044618658097711785492504343953926634992332820282019728792003956564819967",
type(int256).max,
type(int256).max,
155
);
checkParseDecimalFloat(
"1e-57896044618658097711785492504343953926634992332820282019728792003956564819968", 1, type(int256).min, 80
);
checkParseDecimalFloat(
"-57896044618658097711785492504343953926634992332820282019728792003956564819968e-57896044618658097711785492504343953926634992332820282019728792003956564819968",
type(int256).min,
type(int256).min,
157
);

checkParseDecimalFloat("0.0e0", 0, 0, 5);
checkParseDecimalFloat("0.0e1", 0, 1, 5);
checkParseDecimalFloat("1.1e1", 11, 0, 5);
checkParseDecimalFloat("1.1e-1", 11, -2, 6);

// Some negatives.
checkParseDecimalFloat("-1.1e-1", -11, -2, 7);
checkParseDecimalFloat("-10.01e-1", -1001, -3, 9);
}

/// Test some unrelated data after the decimal.
function testParseLiteralDecimalFloatUnrelated() external pure {
checkParseDecimalFloat("0.0hello", 0, 0, 3);
checkParseDecimalFloat("0.0e0hello", 0, 0, 5);
checkParseDecimalFloat("0.0e1hello", 0, 1, 5);
checkParseDecimalFloat("1.1e1hello", 11, 0, 5);
checkParseDecimalFloat("1.1e-1hello", 11, -2, 6);
checkParseDecimalFloat("-1.1e-1hello", -11, -2, 7);
}

/// An empty string should fail.
function testParseDecimalFloatEmpty() external pure {
checkParseDecimalFloatFail("", ParseEmptyDecimalString.selector, 0);
}

/// A non decimal string should revert.
function testParseDecimalFloatNonDecimal() external pure {
checkParseDecimalFloatFail("hello", ParseEmptyDecimalString.selector, 0);
}

/// e without a number should revert.
function testParseDecimalFloatExponentRevert() external pure {
checkParseDecimalFloatFail("e", ParseEmptyDecimalString.selector, 0);
}

/// e with a left digit but not right should revert.
function testParseDecimalFloatExponentRevert2() external pure {
checkParseDecimalFloatFail("1e", MalformedExponentDigits.selector, 2);
}

/// e with a left digit but not right should revert. Add a negative sign.
function testParseDecimalFloatExponentRevert3() external pure {
checkParseDecimalFloatFail("1e-", MalformedExponentDigits.selector, 3);
}

/// e with a right digit but not left should revert.
function testParseDecimalFloatExponentRevert4() external pure {
checkParseDecimalFloatFail("e1", ParseEmptyDecimalString.selector, 0);
}

/// e with a right digit but not left should revert.
/// two digits.
function testParseLiteralDecimalFloatExponentRevert5() external pure {
checkParseDecimalFloatFail("e10", ParseEmptyDecimalString.selector, 0);
}

/// e with a right digit but not left should revert.
/// two digits with negative sign.
function testParseLiteralDecimalFloatExponentRevert6() external pure {
checkParseDecimalFloatFail("e-10", ParseEmptyDecimalString.selector, 0);
}

/// Dot without digits should revert.
function testParseLiteralDecimalFloatDotRevert() external pure {
checkParseDecimalFloatFail(".", ParseEmptyDecimalString.selector, 0);
}

/// Dot without leading digits should revert.
function testParseLiteralDecimalFloatDotRevert2() external pure {
checkParseDecimalFloatFail(".1", ParseEmptyDecimalString.selector, 0);
}

/// Dot without trailing digits should revert.
function testParseLiteralDecimalFloatDotRevert3() external pure {
checkParseDecimalFloatFail("1.", MalformedDecimalPoint.selector, 2);
}

/// Dot e is an error.
function testParseLiteralDecimalFloatDotE() external pure {
checkParseDecimalFloatFail(".e", ParseEmptyDecimalString.selector, 0);
}

/// Dot e0 is an error.
function testParseLiteralDecimalFloatDotE0() external pure {
checkParseDecimalFloatFail(".e0", ParseEmptyDecimalString.selector, 0);
}

/// e dot is an error.
function testParseLiteralDecimalFloatEDot() external pure {
checkParseDecimalFloatFail("e.", ParseEmptyDecimalString.selector, 0);
}

/// Negative e with no digits is an error.
function testParseLiteralDecimalFloatNegativeE() external pure {
checkParseDecimalFloatFail("0.0e-", MalformedExponentDigits.selector, 5);
}

/// Negative frac is an error.
function testParseLiteralDecimalFloatNegativeFrac() external pure {
checkParseDecimalFloatFail("0.-1", MalformedDecimalPoint.selector, 2);
}

/// Can't have more than max total precision. Add decimals after the max int.
function testParseLiteralDecimalFloatPrecisionRevert0() external pure {
checkParseDecimalFloatFail(
"57896044618658097711785492504343953926634992332820282019728792003956564819967.1",
ParseDecimalPrecisionLoss.selector,
79
);
}

/// Can't have more than max total precision. Have an int that makes it
/// impossible to fit the max decimals.
function testParseLiteralDecimalFloatPrecisionRevert1() external pure {
checkParseDecimalFloatFail(
"1.57896044618658097711785492504343953926634992332820282019728792003956564819967",
ParseDecimalPrecisionLoss.selector,
79
);
}
}