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

Added some new string manipulation stocks #1224

Closed
wants to merge 5 commits into from

Conversation

milutinke
Copy link

@milutinke milutinke commented Apr 7, 2020

Added the following stocks:

  • IsStringNumber
  • IsStringFloat
  • StringToLowerCase
  • StringToUpperCase
  • IndexOfChar
  • LastIndexOfChar
  • CopyC

Tested:
Test plugin source link: https://pastebin.com/hqfSZNxY
Test results: https://i.imgur.com/4CawNKq.png

Added the following stocks:
- IsStringNumber
- IsStringFloat
- StringToLowerCase
- StringToUpperCase
- IndexOfChar
- LastIndexOfChar

Tested:
Test plugin source link: https://pastebin.com/QWXUy0ta
Test results: https://i.imgur.com/NKhvsl4.png
* @param szString Input string
* @return Boolean
*/
stock bool IsStringNumber(const char[] szString)
Copy link
Member

@dvander dvander Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need a temporary string? I think it would work to skip past spaces and then call StringToIntExt() and check the return value.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never heard of StringToIntExt and I cannot find it in the documentation.
The reason I used a temporary value is to avoid modifying the original string when trimming.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, StringToIntEx. You don't need to modify the string if you skip spaces with a counter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I tried with StringToInt and StringToFloat and it did not work properly, the test which needed to pass, have failed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Results of those functions: https://i.imgur.com/4l1JzGd.png

{
int iIterator = 0;
int iLength = strlen(szInput);
char[] szOutput = new char[iLength + 1];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is a temporary needed here, given that the string is modified in place?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, they are not needed. I am going to correct it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

{
int iIterator = 0;
int iLength = strlen(szInput);
char[] szOutput = new char[iLength + 1];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above.

int iP;
int iLength = strlen(szString);

while (0 < szString[iIterator] <= 255) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be much easier to write this as a wrapper around StringToFloatEx(), which will also make sure the parsing is consistent.

Removed unnecessary temporary variables.
Copy link
Member

@KyleSanderson KyleSanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've rolled these myself, but they can definitely be simpler and wrapped around our natives. so, + dvander's comments we already have some of this in the tree.

plugins/include/string.inc Show resolved Hide resolved
plugins/include/string.inc Show resolved Hide resolved
@milutinke
Copy link
Author

I have simplified the StringToUpper and StringToLower case.
Regarding the FindCharInString, I did not know about that function, I can change IndexOfChar and LastIndexOfChar to wrap the FindCharInString.
IsStringNumber and IsStringFloat do not work intended when they wrap StringToInt and StringToFloat. Results of the test with those: https://i.imgur.com/4l1JzGd.png

@peace-maker
Copy link
Member

That's why dvander suggested using the Ex variants of those natives. They return the number of characters consumed for the number, so checking if the whole string was consumed does what you want.

stock bool IsStringInteger(const char[] str, int nBase=10)
{
	int result;
	return StringToIntEx(str, result, nBase) == strlen(str);
}

stock bool IsStringFloat(const char[] str)
{
	float result;
	return StringToFloatEx(str, result) == strlen(str);
}

Copy link
Author

@milutinke milutinke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why dvander suggested using the Ex variants of those natives. They return the number of characters consumed for the number, so checking if the whole string was consumed does what you want.

stock bool IsStringInteger(const char[] str, int nBase=10)
{
	int result;
	return StringToIntEx(str, result, nBase) == strlen(str);
}

stock bool IsStringFloat(const char[] str)
{
	float result;
	return StringToFloatEx(str, result) == strlen(str);
}

Yeah, but IsStringFloat fails and counts -1 and -3 as floats, and they are not.
Edit: Nevermind, fixed:

stock bool IsStringFloat(const char[] str)
{
	float result;
	return StringToFloatEx(str, result) == strlen(str) && FindCharInString(str, '.') != -1;
}

* @param szString Input string
* @return Boolean
*/
stock bool IsStringNumber(const char[] szString)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I tried with StringToInt and StringToFloat and it did not work properly, the test which needed to pass, have failed.

{
int iIterator = 0;
int iLength = strlen(szInput);
char[] szOutput = new char[iLength + 1];
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, they are not needed. I am going to correct it.

* @param szString Input string
* @return Boolean
*/
stock bool IsStringNumber(const char[] szString)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Results of those functions: https://i.imgur.com/4l1JzGd.png

{
int iIterator = 0;
int iLength = strlen(szInput);
char[] szOutput = new char[iLength + 1];
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@dvander
Copy link
Member

dvander commented Apr 10, 2020

If you want to be very strict, you can do:

stock bool IsStringFloat(const char[] str)
{
	float result;
	return StringToFloatEx(str, result) == strlen(str) && !IsStringInt(str);
}

But I'm not sure I'm in favor of this. Integers are a subset of floats, and it's kind of arbitrary to demand that decimals are added for whole numbers.

Changed IsStringNumber and IsStringFloat to wrap StringToIntEx and StringToFloatEx.
Changed IndexOfChar to wrap FindCharInString.
Added CopyC
@milutinke
Copy link
Author

milutinke commented Apr 10, 2020

Changed the code to apply your advices.

Changed IsStringNumber and IsStringFloat to wrap StringToIntEx and StringToFloatEx.
Changed IndexOfChar to wrap FindCharInString.
Added CopyC

Tested, everything works:
Test plugin source link: https://pastebin.com/hqfSZNxY
Test results: https://i.imgur.com/4CawNKq.png

@Headline Headline added the Feature Request user requested feature label May 8, 2020
If the string has a floating point value or not, we can still store it in a float.
No need to check the additional stocks, they're built-in.
CopyC already exists, that's simply strcopy.
@KyleSanderson
Copy link
Member

@milutinke I've made some adjustments that I thought were appropriate. What do you think?

@KyleSanderson
Copy link
Member

@milutinke any update on this?

@milutinke
Copy link
Author

milutinke commented May 21, 2021

@milutinke any update on this?

Sorry for replying this late, I've not received a notification about this for some reason back then.
CopyC does not exist in Source Mod, at least to my knowledge.
Also, I think that return StringToFloatEx(str, result) == strlen(str); will not work properly in IsStringFloat without && FindCharInString(str, '.') != -1; if I remember correctly.

@sirdigbot
Copy link
Contributor

If the goal is to add more string stocks for common stuff, I feel like borrowing a few more ideas from python would make sense.
Such as IsStringAlpha/Numeric/AlNum, IsStringASCII, IsStringPrintable, StringEndsWith.
There's already char versions of some of those, so making string equivalents doesn't seem unreasonable.

@Alienmario
Copy link
Contributor

If the goal is to add more string stocks for common stuff, I feel like borrowing a few more ideas from python would make sense. Such as IsStringAlpha/Numeric/AlNum, IsStringASCII, IsStringPrintable, StringEndsWith. There's already char versions of some of those, so making string equivalents doesn't seem unreasonable.

+1 I've missed StringEndsWith in almost every other plugin I made.

@Alienmario
Copy link
Contributor

Sorry to bump, can we get some update on this? I feel like converting String case is a very fundamental operation and it would be nice to have SM ship with it.

@peace-maker
Copy link
Member

Would adding wrappers for other available stock functions really help or make the API more confusing?

I've reviewed this again and am in favor of adding more string utility stocks as suggested, but am hesitant on adding wrappers like IndexOfChar -> FindCharInString. Having multiple functions doing the same thing doesn't help readability of code. But FindCharInString is a mouthful and not the best choice of name for what it does after all.

@milutinke
Copy link
Author

Would adding wrappers for other available stock functions really help or make the API more confusing?

I've reviewed this again and am in favor of adding more string utility stocks as suggested, but am hesitant on adding wrappers like IndexOfChar -> FindCharInString. Having multiple functions doing the same thing doesn't help readability of code. But FindCharInString is a mouthful and not the best choice of name for what it does after all.

People who come from other programming languages are used to IndexOfChar name, so it would not hurt having it in my opinion.
But yeah, I am fine without it being added too.

@sirdigbot
Copy link
Contributor

sirdigbot commented Jul 5, 2023

I have an assortment of issues with the current functions, but I'm told I should put the improvements here instead of making a competing PR.
Sorry in advance.

Bug

  • IsStringNumber/IsStringFloat both incorrectly consider an empty string as valid.

Naming

  • IsStringNumber only detects integers so it should be called IsStringInteger
  • IsStringFloat is misleading. As dvander mentioned it should (and does) handle integer values too (usually what's important is "can it convert to a float"). IsStringDecimal would be better as StringToFloat requires base-10 numbers anyway.
  • StringToLowerCase should be StringToLower both because it's shorter and because it matches IsCharLower and CharToLower
  • Ditto for StringToUpperCase

Style

  • Hungarian notation (prefixing with the type) should be removed from params and names should be made more consistent with the other functions. e.g. szInput -> str
  • Variables also should not use hungarian notation
  • EOS is never used anywhere in sourcemod's codebase except a single test case. I'm not sure if it's considered bad to use compared to '\0' but it might be.
  • Both the StringToLower/Upper functions could've just been for-loops for readability sake: for (int i = 0; str[i] != '\0'; ++i) is a common idiom.
  • In IndexOfChar's docstring, "Do we need the last index (Optional, default: false)" should probably borrow the description from FindCharInString's parameter: "False (default) to search forward, true to search backward."

Misc

  • IsStringNumber is missing the nBase parameter in its docstring
  • Docstrings have incorrect @return descriptions--the meaning of the return values (not the type) is supposed to be there. So for IsStringFloat for example, @return Boolean should be changed to something like @return True if string is a valid float number.
  • @return is not needed for void functions.
  • "Input string for conversion and also the output" is unclear and wordy. You could borrow from CharToLower/Upper: "Converts each character in a string to its lowercase counterpart."
  • "Supports negative values" should not need to be clarified for IsStringNumber/IsStringFloat since not supporting them is incorrect.
  • The docstrings for every single function have the incorrect parameter names for every parameter.
  • For IndexOfChar, "index of the found character" is unclear because it actually finds the first occurrence of the character.
  • (Picky) const char cCharacter doesn't really need to be const since scalar values are always passed by-value (i.e. copied) anyway.

@Alienmario
Copy link
Contributor

@sirdigbot Maybe you should make a PR after all, as this one appears cursed.
I can help providing some stocks I've been using.

@sirdigbot
Copy link
Contributor

@sirdigbot Maybe you should make a PR after all, as this one appears cursed. I can help providing some stocks I've been using.

Sure, go ahead. I'll take a look. Though I already have a massive list of proposed stocks from the last time I had a go at a PR.

@milutinke
Copy link
Author

For some reason I did not get notifications until now (all of them) for this PR even I'm subscribed.
Since I've stopped doing any CS:GO related stuff years ago, I'll close the PR, thus a new one can be opened with the proposed changes.

@milutinke milutinke closed this Jun 28, 2024
@Alienmario
Copy link
Contributor

Sure, go ahead. I'll take a look. Though I already have a massive list of proposed stocks from the last time I had a go at a PR.

stock bool StrEndsWith(const char[] str, const char[] suffix, bool caseSensitive = true)
{
	return StrEndsWithEx(str, strlen(str), suffix, caseSensitive);
}

stock bool StrEndsWithEx(const char[] str, int realLen, const char[] suffix, bool caseSensitive = true)
{
	int suffixLen = strlen(suffix);
	return (realLen >= suffixLen && StrEqual(str[realLen - suffixLen], suffix, caseSensitive));
}

// Non-ex func already used by arraylist
stock int FindStringInArrayEx(const char[][] array, int len, const char[] str, bool caseSensitive = true)
{
	for (int i = 0; i < len; i++)
	{
		if (StrEqual(str, array[i], caseSensitive))
		{
			return i;
		}
	}
	return -1;
}

// Non-ex funcs already used by arraylist
stock int FindValueInArrayEx(const any[] array, int len, const any val)
{
	for (int i = 0; i < len; i++)
	{
		if (array[i] == val)
		{
			return i;
		}
	}
	return -1;
}

stock int FindCharInArray(const any[] array, int len, const int val, bool caseSensitive = true)
{
	int i = FindValueInArrayEx(array, len, val);
	if (!caseSensitive && i == -1)
	{
		int val2 = IsCharLower(val) ? CharToUpper(val) : CharToLower(val);
		if (val2 != val)
		{
			i = FindValueInArrayEx(array, len, val2);
		}
	}
	return i;
}

// Like SplitString, but more efficient?
stock int SplitByChar(const char[] szSource, char c, char[] szBuffer, int nSize)
{
	int i = 0;
	while (szSource[i] != c)
	{
		if (szSource[i] == '\0')
		{
			return -1;
		}
		i++;
	}
	i++;
	strcopy(szBuffer, i < nSize ? i : nSize, szSource);
	return i;
}

If you pick any, I can clean them up and add docs. I especially like StrEndsWithEx as it lets me check for multiple file extensions without calculating string length repeatedly.

Additionally I think FindCharInString stock can be optimized (in forward mode at least) by removing strlen at the beginning and checking for end of string inside the loop.

@sirdigbot
Copy link
Contributor

sirdigbot commented Jun 29, 2024

StringEndsWith is already on my big list, as is StringStartsWith
StrEndsWithEx I feel is too specific and your use case is better replaced by regex.
FindStringInArrayEx is alright, but it might not be enough to justify a stock. I'll add it to my list anyway and let the maintainers decide.
FindValueInArrayEx and FindCharInArray aren't really string related as far as I can tell. Not sure of their use case.
SplitByChar isn't needed. SplitString may not be the perfectly efficient but the stocks are more meant for handling common operations more than micro-optimising stuff. At least in my opinion

@Alienmario
Copy link
Contributor

Alienmario commented Jun 29, 2024

StrEndsWithEx I feel is too specific and your use case is better replaced by regex.

Here is my use case https://github.com/Alienmario/smartdm-redux/blob/main/scripting/include/smartdm_redux.inc#L110
I feel like regex is a overkill for such a simple function.
The gains may be smaller than expected, but I like having the option.
Keep in mind strlen computation time grows with length of string.

@sirdigbot
Copy link
Contributor

StrEndsWithEx I feel is too specific and your use case is better replaced by regex.

Here is my use case https://github.com/Alienmario/smartdm-redux/blob/main/scripting/include/smartdm_redux.inc#L110 I feel like regex is a overkill for such a simple function. The gains may be smaller than expected, but I like having the option. Keep in mind strlen computation time grows with length of string.

Yeah so this seems to just be avoiding needing to call strlen(filepath) each time you check a different file extension. Which is technically an optimisation but probably not enough of one to be built into sourcemod directly. Especially because you're still calling strlen(extension), where the slowest component of that is going to be the SP -> C++ -> SP process anyway.

While strlen is O(n), 5 redundant calls to it on even a long filepath are probably insignificant unless it's happening pretty frequently.

@sirdigbot
Copy link
Contributor

Anyway, I've started a new PR where further comments should be added instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature Request user requested feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants