AppleScript math error. (possible work around?)

Hi everyone!

I have a script which detects when thenum is 0 but here’s a short form:

set thenum to 1
repeat
	set thenum to thenum - 0.05
	display dialog thenum
end repeat

it goes from 0.95 to 0.8999999999999999

Any work arounds ?

Thanks

Hi,

scripter error, no math error :wink:

from the dictionary
display dialog unicode text

set thenum to 1
repeat
	set thenum to thenum - 0.05
	display dialog thenum as text
end repeat

Thanks for your very quick reply, thought I outsmarted AppleScript :slight_smile: but why does it go from 0,05 to -3,191891195797E-16

I know it’s ASS but this works though (although it gives an error at (window of theObject) but that has nothing to do with the zero thing)

on should close theObject
	set theAlpha to 1
	repeat
		set theAlpha to theAlpha - 0.05
		set alpha value of window (window of theObject) to theAlpha
		if theAlpha as text is equal to "-3,191891195797E-16" as text then
			close panel (window of theObject)
			set alpha value of window (window of theObject) to 1
			exit repeat
		end if
	end repeat
end should close

workaround


set thenum to 1 as real
repeat
	set thenum to (round ((thenum - 0.05) * 100)) / 100
	display dialog thenum as text
end repeat

Edit: Why not


on should close theObject
	set theAlpha to 1 as real
	repeat
		set theAlpha to theAlpha - 0.05
		set alpha value of window (window of theObject) to theAlpha
		if theAlpha is 0.05 then
			close panel (window of theObject)
			set alpha value of window (window of theObject) to 1
			exit repeat
		end if
	end repeat
end should close

Thanks it works :smiley:

Like many other programming languages, floating point in AppleScript is not actually decimal based. Practically all modern machines use the binary variants of IEEE 754 and many programming languages’ either specify an IEEE 754 floating point behavior or just use whatever the host machine’s hardware does. However AppleScript arrives there (by specification or by default), it appears to use some variation of IEEE 754 64-bit binary (“double precision”) floating point.

Because of its binary nature, decimal values like 0.05 can not be exactly represented in an AppleSript floating point value (because 10 (the base of our normal number system) is not a power of 2 (the base of the binary floating point system used in most computers)).

The unexpected values you saw were due to the limited precision of the floating point system and rounding errors introduced at each subtraction.

The workaround of converting the value to text and comparing it to a particular string value may work OK, but it is a bit brittle (it depends on the floating point implementation, the floating point rounding mode, and the international decimal separator). A simple solution is to make a comparison like theAlpha <= 0. This will trigger whether the value exactly hits zero or whether it goes slightly negative due to rounding errors. Comparisons like this are often a good idea even if you are only dealing with integers. If there is a bug and you accidentally overshoot your target value an equality comparison can result in an infinite loop while an inequality will terminate even if the value does not match exactly.

to getTempFile()
    POSIX file (POSIX path of (path to temporary items folder as alias) & "double.dat")
end getTempFile
to writeRealsToFile(rl, f)
    local r, fd, m, n
    set rl to rl as list
    repeat with r in rl
        set contents of r to r as real
    end repeat
    set r to r as real
    set fd to open for access f with write permission
    try
        repeat with r in rl
            write r to f as real
        end repeat
        close access fd
    on error m number n
        try
            close access fd
        end try
        error m number n
    end try
end writeRealsToFile
to getRealsInHex(rl)
    set f to getTempFile()
    writeRealsToFile(rl, f)
    paragraphs of (do shell script "hexdump -v -e '8/1 \"%02X\" \"\\n\"' < " & POSIX path of f) -- hex dump 8 bytes per line (8 bytes = 64 bits = size of a double precision float)
end getRealsInHex

set values to {1}
repeat until item -1 of values ≤ 0
    set end of values to (item -1 of values) - 0.05
end repeat

set end of values to 0.05
getRealsInHex(values)
(*
{"3FF0000000000000",
 "3FEE666666666666",
 "3FECCCCCCCCCCCCC",
 "3FEB333333333332",
 "3FE9999999999998",
 "3FE7FFFFFFFFFFFE",
 "3FE6666666666664",
 "3FE4CCCCCCCCCCCA",
 "3FE3333333333330",
 "3FE1999999999996",
 "3FDFFFFFFFFFFFF9",
 "3FDCCCCCCCCCCCC6",
 "3FD9999999999993",
 "3FD6666666666660",
 "3FD333333333332D",
 "3FCFFFFFFFFFFFF4",
 "3FC999999999998E",
 "3FC3333333333328",
 "3FB9999999999983",
 "3FA999999999996C", -- the last value greater than zero, not quite the same as "direct 0.05"
 "BCB7000000000000", -- the first value <= zero
 "3FA999999999999A" -- "direct 0.05"
 }
*)

[code]IEEE 754 64-bit “double float” for 0.05: 3FA999999999999A

     raw = hex 3FA999999999999A
           bits 0011111110101001100110011001100110011001100110011001100110011010
    sign = 0
           bits 0

raw exponent = 1018
bits 01111111010
significand = 2702159776422298
bits 1001100110011001100110011001100110011001100110011010

Note the rounding error in the significand. It should have a pattern of repeating 0011, but after the last full 0011 repetition, it ends with 010. When it gets to the end, it only had three bits left for the last repetition, so it was rounded off.

exponent = raw_exponent - bias
= 1018 - 2^(11-1) -1
= 1018 - 1023
= -5

value = (-1)^sign * (1 + significand * 2^52) * 2^exponent
= 1 * (1 + significand / 2^52) * 2^exponent
= 2^exponent + significand * 2^exponent * 2^-52
= 2^exponent + significand * 2^(-52 + exponent)
= 2^-5 + significand * 2^-57
= 1/32 + 2702159776422298 * 1/144115188075855872
= 0.03125 + 0.01875000000000000277555756156289135105907917022705078125
= 0.05000000000000000277555756156289135105907917022705078125

The last value > 0 found by starting with 1 and repeatedly subtracting 0.05: 3FA999999999996C
value = 1 * (1/32 + 2702159776422252 * 1/144115188075855872)
= 0.0499999999999996835864379818303859792649745941162109375

Because of the repeated subtraction, the rounding error has progressed to affect 7 bits instead of the 2 bits present in the original value for 0.05. Both of these values display as “0.05”, but they are distinct values, which is why you get the following value when you subtract 0.05 once more.

The first value <= 0 found by starting with 1 and repeatedly subtracting 0.05: BCB7000000000000
value = -1 * (1/4503599627370496 + 1970324836974592 * 1/20282409603651670423947251286016)
= -0.00000000000000031918911957973250537179410457611083984375[/code]

set r to "9007199254740992" as real
r is equal to r + 1 --> true !!!

Edit History: Added script to demo r = r + 1 (different problem, but shows that floating point values are not normal numbers).

Thanks for the info :slight_smile:

set r to "9007199254740992" as real
r is equal to r + 1 --> true !!!

Is quite impressive :smiley:

I’d think that Apple would use the same floating point in all applications. If you do the same in Calculator ( -0.05), the result is correct. Ofcourse, the Calculator is “smarter” than AppleScript but I didn’t expect the difference.

So if I use theAplha - 0.125, it’s correct because 0.125 is a power of 2 right ?

About your work around, I think I’ll go with this,

on should close theObject
   set theAlpha to 1 as real
   repeat
       set theAlpha to theAlpha - 0.05
       set alpha value of window (name of theObject) to theAlpha
       if theAlpha ≤ 0.05 then
           set visible of window (name of theObject) to false
           set alpha value of window (name of theObject) to 1.0
           exit repeat
       end if
   end repeat
end should close

theAplha can’t be negative, it has to be a value between 0 & 1

Do you think it’s “safe” on both Tiger & Leopard and with both “.” & “,” between the “0” and the “05” ?

Thanks :slight_smile:

I think some calculator-type apps use custom software-based decimal floating-point libraries instead of using the binary floating point hardware. This provides a match for how users expect the operations and numbers to behave and it is OK if it is not super fast since the use is all interactive (no one would use Calculator to plot fractals, or do physics simulations). Although I do have a faint memory of a Software Update that fixed some kind of numeric bug in Calculator.

If by correct you mean that you will get to exactly 0 after 8 subtractions, then yes (the same for all 2^-n where 0 ≤ n ≤ 53; the smallest such value would take 9007199254740992 subtractions to get to zero, so I have not tested it). But still, it is probably best to avoid making tests that rely on such exactness.

I do not have any experience using any fraction separator other than “.”. My impression is that AppleScript program code always uses “.”. The locale/international settings are only used when converting to or from text (reading input from the user, putting values into a string for display to the user, etc.). So as long as you do not convert to/from text (realValue as text/string/unicode text or stringValue as real) I think it will be safe from locale/international issues (the bits are the same, it is just how we write the numbers that varies).

OK thanks :slight_smile: I asked it because here in Belgium, we write “0.05” as “0,05” and maybe there would be a difference. But I’ll test it later on. I don’t have Dutch (what I normally speak) installed on my Mac so I’ll have to search for my disk and install it and then test it :slight_smile: But I guess that it is the same.