GameMaker: Checking whether a string is a valid number


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!

Related posts:

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.