packages/solidity-devops/src/libs/StringUtils.sol
// 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);
}
}