Some things are numbers, some aren't
GameMaker Studio 2.2.2 released few days ago, bringing, among improvements, "GML consistency", which changes how automatic type conversion works in corner cases.
A little less known thing, together with that it also changed how GameMaker's string-to-number conversion function (real) works, having it throw an error if your string is definitely not a number.
A slight inconvenience, given that there is not a function to check if a string is a number before attempting conversion.
But, of course, that can be fixed.
What is a number
For all we know, a string containing a number would have the following structure:
- A minus sign - (optional)
- Zero or more digits
- A period . (optional)
- If period is included, zero or more digits
- Must contain at least 1 digit total
- Must not contain anything else (trim your stirng separately if you must)
Let's break this down in steps,
An unsigned integer
This one's easy because GameMaker has a built-in string_digits function, which will take a string and return a new string that only contains digits from it ("a4b5" -> "45"). Thus we can utilize this to check whether a string only contains digits (and also that it is not empty):
/// string_is_uint(string) var s = argument0; var n = string_length(string_digits(s)); return n > 0 && n == string_length(s);
As we know that string_digits will return the digits in order,
a length comparison will suffice.
Nice and easy.
A signed integer
The only difference between a signed an unsigned integer is that a signed one might have a - in front. So, we need to check that it's either all-digits, or (number of digits - 1) long if there's a -.
As GameMaker allows to implicitly cast true to 1 and false to 0, we can cheat just a little bit:
/// string_is_int(string) var s = argument0; var n = string_length(string_digits(s)); return n > 0 && n == string_length(s) - (string_ord_at(s, 1) == ord("-"));
(to be fair, we could also make use of fact that GM's "truthfulness" condition for numbers is num > 0.5 and shorten that to return n && ..., but let's stick to clearer notation here)
A floating-point number
Things are exactly the same, but! There can now be a dot/period.
The implementation is pretty lean about this - 1.1, 1., and .1 are all valid numbers.
So we can simply check if the input contains a ., and further decrease the expected number of digits if that is so:
/// string_is_real(string) var s = argument0; var n = string_length(string_digits(s)); return n > 0 && n == string_length(s) - (string_ord_at(s, 1) == ord("-")) - (string_pos(".", s) != 0);
Exponential notation
Did you know that GameMaker allows exponential notation for values passed to real?
1e3 for 1000 (1*103) or .1e2 for 10 (0.1*102) and such.
Not actively documented or anything.
And I don't suppose you would want to let the user enter such values often either.
But still, if you'd want that,
/// string_is_real_exp(string) var s = argument0; var n = string_length(string_digits(s)); var p = string_pos(".", s); var e = string_pos("e", s); switch (e) { case 0: break; // ok! case 1: return false; // "e#" case 2: if (p > 0) return false; break; // ".e#" or "1e." default: if (p > 0 && e < p) return false; break; // "1e3.3" } return n && n == string_length(s) - (string_char_at(s, 1) == "-") - (p != 0) - (e != 0);
Doing it yourself
Suppose you want to do things yourself, without utilizing string_digits. You can do that too,
/// string_is_real_exp_pure(string) var s = argument0; var n = string_byte_length(s); var seenDot = false; var seenExp = false; var numDigs = 0; var i = 1; if (string_byte_at(s, 1) == ord("-")) i += 1; while (i <= n) { var c = string_byte_at(s, i); switch (c) { case ord("."): if (seenDot || seenExp) return false; seenDot = true; break; case ord("e"): case ord("E"): if (seenExp || numDigs == 0) return false; seenExp = true; break; default: if (c >= ord("0") && c <= ord("9")) { numDigs += 1; } else return false; } i += 1; } return numDigs > 0;
As a note here, if you are reading this not for GM, pay attention that GML strings have indexes start at 1, so you'll need i = 0, (s, 0), and i < n accordingly.
As a second note, on HTML5 it's beneficial to use string_ord_at+string_length instead of string_byte_at+string_byte_length because JS doesn't work with bytes directly.
Have fun!