Twine (Harlowe) maths a little funky - not sure what I've done wrong

If you are requesting technical assistance with Twine, please specify:
Twine Version: 2.4.0
Story Format: Harlowe 3.3.0

I was trying to do some (hopefully very simple) maths in my story, but I clearly don’t understand even simple maths, because it’s gone terribly skew-whiff.

I wanted to assign a variable ($vol1) to any of a set of possible numbers and then multiply that number by 23. (Simple, right?)

I set up my code like this:

(set: $vol1 to (either: 0.1, 0.25, 0.50, 0.75, 1.1, 1.5, 2.0))
(set: $a1 to (23 * $vol1))
(set: $a2 to (string: $a1))

(print: $vol1) - this is the vol value that was selected <br>
(print: $a1) - this is  variable a1 <br>
(print: $a2) - should be a string of a1

and then tested it out. It works correctly most of the time (by which, I mean that it multiplies $vol1 by 23, and spits that out as $a1 and $a2 (just double-checking, lol).

(For example:
0.25 - this is the vol value that was selected

5.75 - this is variable a1

5.75 - should be a string of a1
and that is what I expected it to print.)

This code doesn’t, however, work correctly when $vol1 is chosen as 0.1. It outputs this instead:

0.1 - this is the vol value that was selected

2.3000000000000003 - this is variable a1

2.3000000000000003 - should be a string of a1

…and then at this point I started questioning my sanity (and understanding of maths), because why is $a1 not 2.3?

If I remove 0.1 from the possible options for $vol1, all of the other numbers still work as previously, so I guess it must be something special about 0.1 as a number (and not its position in the list of options?)

I guess it isn’t a huge deal in the scope of things (I can take out 0.1 from the list without any problems) - but what did I do wrong, and what is different about 0.1? Are there other numbers I should avoid because they’ll return the same problem?

Thanks in advance for any help - I know it’s an incredibly stupid question, probably I should really be sent back to a basic maths class but I can’t seem to figure this out on my own!

This is just the way decimal math works on computers. Most values can’t be exactly represented, so you get these slight errors.

Decimal numbers in JavaScript are in the IEEE 754 floating-point format, which sacrifices some accuracy for increased range. That’s why you can get results that are off of what you’d expect by very minor amounts.

If you need more accurate decimal numbers, the usual solution is to use whole numbers (integers) instead. Basically, multiply all of the numbers by whatever multiple of 10 will make them all whole numbers and work with those. Whenever you need to show the player a decimal version, just divide by whatever multiple of 10 you used prior.

For example:

<!-- Multiplied by 100 to make all of these whole numbers. -->
(set: $vol1 to (either: 10, 25, 50, 75, 110, 150, 200))

(set: $a1 to 23 * $vol1)
(set: $a2 to (string: $a1 / 100))

(print: $vol1 / 100) - this is the vol value that was selected <br>
(print: $a1 /100) - this is  variable a1 <br>
(print: $a2) - should be a string of a1

I’m not entirely sure that’s all valid Harlowe—unsure if arithmetic is allowed within (string:) and (print:)—but you should get the idea.

 

To be pedantic—please forgive me. While generally correct, that’s not entirely accurate.

It’s how floating-point math works—e.g., IEEE 754, which underpins most floating-point types today. There are both languages with fixed-point types and fixed-point libraries that work as most laymen would expect. They’re rare, but do exist.

1 Like

That has solved the problem beautifully, thank you so much! (It does work with the arithmetic being done in the string and print macros, too.)

I’d been starting to think that either I was going mad or something, so it’s a huge relief to know that it’s a general feature of JavaScript IEEE and not just me. A little unclear still on why the problem was only triggered with 0.1 and not any of the other decimals I’d used, but I’ll just use whole numbers exclusively from now on to try to avoid the problem.

Thanks again, I really appreciate your help!

From the article to which @TheMadExile referred:

Whether or not a rational number has a terminating expansion depends on the base. For example, in base-10 the number 1/2 has a terminating expansion (0.5) while the number 1/3 does not (0.333…). In base-2 only rationals with denominators that are powers of 2 (such as 1/2 or 3/16) are terminating. Any rational with a denominator that has a prime factor other than 2 will have an infinite binary expansion. This means that numbers that appear to be short and exact when written in decimal format may need to be approximated when converted to binary floating-point. For example, the decimal number 0.1 is not representable in binary floating-point of any finite precision; the exact binary representation would have a “1100” sequence continuing endlessly:

e = −4; s = 1100110011001100110011001100110011…,

where, as previously, s is the significand and e is the exponent.

When rounded to 24 bits this becomes

e = −4; s = 110011001100110011001101,

which is actually 0.100000001490116119384765625 in decimal.

1 Like

I stand by my one-line summary. :) On the grounds that (a) programming newcomers will run into floating-point math first and always, unless they do a lot of digging, and (b) before they do the digging, they won’t know what “floating-point” means.

That makes so much sense now, thank you! :slight_smile:

Just to be contrarian, I’ll mention decimal floating point.