synapsecns/sanguine

View on GitHub
packages/solidity-devops/src/libs/StringUtils.sol

Summary

Maintainability
Test Coverage
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.2 <0.9.0;

/// @notice Library for handling strings, that could be used with any Solidity version from 0.6 to 0.8.
/// @author Synapse Contributors
library StringUtils {
    /// @dev The value returned by indexOf when the substring is not found.
    uint256 internal constant NOT_FOUND = type(uint256).max;

    bytes1 private constant ZERO = bytes1("0");
    bytes1 private constant NINE = bytes1("9");

    /// @notice Returns the length of a string
    function length(string memory str) internal pure returns (uint256) {
        return bytes(str).length;
    }

    // ══════════════════════════════════════════════════ SLICING ══════════════════════════════════════════════════════

    /// @notice Returns a substring of a string in the range [startIndex, endIndex)
    /// @param str          The string to take a substring of
    /// @param startIndex   The start index (inclusive)
    /// @param endIndex     The end index (exclusive)
    function substring(string memory str, uint256 startIndex, uint256 endIndex) internal pure returns (string memory) {
        // Start index should be within [0, str.length]
        require(startIndex <= bytes(str).length, "StringUtils: Start index out of bounds");
        // End index should be within [0, str.length]
        require(endIndex <= bytes(str).length, "StringUtils: End index out of bounds");
        // Start index should be less than or equal to end index
        require(startIndex <= endIndex, "StringUtils: Invalid range");
        bytes memory strBytes = bytes(str);
        bytes memory result = new bytes(endIndex - startIndex);
        for (uint256 i = startIndex; i < endIndex; i++) {
            result[i - startIndex] = strBytes[i];
        }
        return string(result);
    }

    /// @notice Returns a suffix of a string starting at the given index, inclusive: [startIndex, str.length)
    /// @param str          The string to take a suffix of
    /// @param startIndex   The start index (inclusive)
    function suffix(string memory str, uint256 startIndex) internal pure returns (string memory) {
        return substring(str, startIndex, bytes(str).length);
    }

    /// @notice Returns a prefix of a string ending at the given index, exclusive: [0, endIndex)
    /// @param str          The string to take a prefix of
    /// @param endIndex     The end index (exclusive)
    function prefix(string memory str, uint256 endIndex) internal pure returns (string memory) {
        return substring(str, 0, endIndex);
    }

    // ═══════════════════════════════════════════════ CONCATENATION ═══════════════════════════════════════════════════

    // Note: this is implemented, as `string.concat` is not available until Solidity 0.8.0.

    /// @notice Concatenates two strings
    function concat(string memory a, string memory b) internal pure returns (string memory) {
        return string(abi.encodePacked(a, b));
    }

    /// @notice Concatenates three strings
    function concat(string memory a, string memory b, string memory c) internal pure returns (string memory) {
        return string(abi.encodePacked(a, b, c));
    }

    /// @notice Concatenates four strings
    function concat(
        string memory a,
        string memory b,
        string memory c,
        string memory d
    )
        internal
        pure
        returns (string memory)
    {
        return string(abi.encodePacked(a, b, c, d));
    }

    /// @notice Concatenates five strings
    function concat(
        string memory a,
        string memory b,
        string memory c,
        string memory d,
        string memory e
    )
        internal
        pure
        returns (string memory)
    {
        return string(abi.encodePacked(a, b, c, d, e));
    }

    /// @notice Concatenates six strings
    function concat(
        string memory a,
        string memory b,
        string memory c,
        string memory d,
        string memory e,
        string memory f
    )
        internal
        pure
        returns (string memory)
    {
        return string(abi.encodePacked(a, b, c, d, e, f));
    }

    /// @notice Duplicates a string a given number of times.
    /// Example: duplicate("abc", 3) = "abcabcabc"
    function duplicate(string memory str, uint256 times) internal pure returns (string memory duplicateStr) {
        duplicateStr = "";
        for (uint256 i = 0; i < times; i++) {
            duplicateStr = concat(duplicateStr, str);
        }
    }

    // ════════════════════════════════════════════════ COMPARISON ═════════════════════════════════════════════════════

    /// @notice Checks if two strings are equal
    /// @param a    The first string
    /// @param b    The second string
    function equals(string memory a, string memory b) internal pure returns (bool) {
        return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b));
    }

    /// @notice Returns the index of the first occurrence of a substring in a string, or NOT_FOUND if not found.
    /// @param str       The string to search in
    /// @param subStr    The substring to search for
    function indexOf(string memory str, string memory subStr) internal pure returns (uint256) {
        bytes memory strBytes = bytes(str);
        bytes memory subStrBytes = bytes(subStr);
        if (subStrBytes.length > strBytes.length) {
            return NOT_FOUND;
        }
        for (uint256 startIndex = 0; startIndex <= strBytes.length - subStrBytes.length; ++startIndex) {
            // Check if substring starting from startIndex is equal to subStr
            uint256 endIndex = startIndex + subStrBytes.length;
            if (equals(subStr, substring(str, startIndex, endIndex))) {
                return startIndex;
            }
        }
        return NOT_FOUND;
    }

    /// @notice Returns the index of the last occurrence of a substring in a string, or NOT_FOUND if not found.
    /// @param str       The string to search in
    /// @param subStr    The substring to search for
    function lastIndexOf(string memory str, string memory subStr) internal pure returns (uint256) {
        bytes memory strBytes = bytes(str);
        bytes memory subStrBytes = bytes(subStr);
        if (subStrBytes.length > strBytes.length) {
            return NOT_FOUND;
        }
        for (uint256 endIndex = strBytes.length; endIndex >= subStrBytes.length; --endIndex) {
            // Check if substring ending at endIndex is equal to subStr
            uint256 startIndex = endIndex - subStrBytes.length;
            if (equals(subStr, substring(str, startIndex, endIndex))) {
                return startIndex;
            }
        }
        return NOT_FOUND;
    }

    // ════════════════════════════════════════════ INTEGER CONVERSION ═════════════════════════════════════════════════

    /// @notice Derives integer from its string representation.
    /// @param str  The string to convert
    function toUint(string memory str) internal pure returns (uint256 val) {
        bytes memory bStr = bytes(str);
        for (uint256 i = 0; i < bStr.length; ++i) {
            bytes1 b = bStr[i];
            require(b >= ZERO && b <= NINE, "StringUtils: Not a digit");
            uint8 digit = uint8(b) - uint8(ZERO);
            val = val * 10 + digit;
        }
    }

    /// @notice Converts an integer to its string representation.
    function fromUint(uint256 val) internal pure returns (string memory) {
        // Special case for 0
        if (val == 0) {
            return "0";
        }
        // Calculate length of string
        uint256 len = 0;
        for (uint256 i = val; i > 0; i /= 10) {
            ++len;
        }
        // Populate string in reverse
        bytes memory bStr = new bytes(len);
        for (uint256 i = 0; i < len; ++i) {
            uint8 digit = uint8(val % 10);
            bytes1 char = bytes1(uint8(ZERO) + digit);
            bStr[len - i - 1] = char;
            val = val / 10;
        }
        return string(bStr);
    }

    // ═════════════════════════════════════════════ FLOAT CONVERSION ══════════════════════════════════════════════════

    /// @notice Converts a float to its string representation.
    /// @param val       The float to convert, scaled by 10**decimals
    /// @param decimals  The number of decimals to use
    function fromFloat(uint256 val, uint256 decimals) internal pure returns (string memory) {
        // Get the integer part
        string memory strInt = fromUint(val / 10 ** decimals);
        // Get the fractional part
        string memory strFrac = fromUint(val % 10 ** decimals);
        // Pad fractional part with zeros to match the number of decimals
        while (bytes(strFrac).length < decimals) {
            strFrac = concat("0", strFrac);
        }
        // Concatenate integer and fractional parts
        return concat(strInt, ".", strFrac);
    }

    /// @notice Converts a float to its string representation, using 18 decimals.
    /// @param val   The float to convert, scaled by 10**18
    function fromWei(uint256 val) internal pure returns (string memory) {
        return fromFloat(val, 18);
    }
}