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*10^{3}) or `.1e2` for `10` (0.1*10^{2}) 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*!*