(2.4 == 2.4) is false!

Something is bothering me with my code, extremely, and I can’t figure out why is this happening. Look at the outputs below the code please

(
~arrWithSmallestMaxNum = {
	arg arrs;
	arrs.minItem({arg a, i; a.maxItem})
};
~getArrIdx = {
	arg arrs, arr;
	var idx = -1;
	block {
		arg breakfun;
		arrs.do {
			arg a;
			idx = idx + 1;
			if (a == arr) {breakfun.value(idx)};
		};
		nil
	}
};
~maxCommonN = {
	arg arrs, isInAll;
	var smaxArr = ~arrWithSmallestMaxNum.(arrs);
	var idxToRm = ~getArrIdx.(arrs, smaxArr);
	// Modify arrs
	if (idxToRm != nil) {arrs.removeAt(idxToRm)};
	block {
		arg break;
		smaxArr[1..].do {
			arg n;
			isInAll = true;
			arrs.do {
				arg arr;
				arr = arr[1..];
				arr.do{
					arg a;
					[arr, a, n, (a == n)].postln;
				};
				if (arr.includes(n).not) {isInAll = false}
			};
			if (isInAll) {break.value(n)};
		};
		nil
	}
};
)

Now calling it:

(
~maxCommonN.([
	Array.series(5, 0, 3/5),
	Array.series(5, 0, 4/5)
]);
)

outputs:

[ [ 0.8, 1.6, 2.4, 3.2 ], 0.8, 0.6, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 1.6, 0.6, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 2.4, 0.6, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 3.2, 0.6, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 0.8, 1.2, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 1.6, 1.2, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 2.4, 1.2, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 3.2, 1.2, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 0.8, 1.8, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 1.6, 1.8, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 2.4, 1.8, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 3.2, 1.8, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 0.8, 2.4, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 1.6, 2.4, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 2.4, 2.4, false ]
[ [ 0.8, 1.6, 2.4, 3.2 ], 3.2, 2.4, false ]

In the line before the last line, a and n are both 2.4 and I asked whether a == n, with the result being false! Also a === n returns false. Why is this happening. I can’t see it in the code. Any help is appreciated.

2.4 is a finite decimal fraction, but an infinitely-repeating binary fraction.

That is, when SC prints 2.4, it’s a little bit lying to you. There are many 64-bit floats that will print as 2.4, and == doesn’t look at how the numbers print; it looks at the exact sequence of bits.

One way to handle this is to use fuzzyEqual to do “equals within a given precision.”

hjh

But then why does this return true, it’s basically what is happening in the code too:

Array.series(5, 0, 3/5).includes(2.4)

Because the binary representations of (3/5) + (3/5) + (3/5) + (3/5) and (4/5) + (4/5) + (4/5) are not the same. The former matches the binary representation of the literal 2.4; the latter is different.

When they are printed, they both round off to 2.4, but they are in fact not equal.

It’s similar to the way that (1/3) + (1/3) is 0.666666, but 2/3 may evaluate to 0.666667.

hjh

1 Like

here’s a workaround:

(
~maxCommonN.([
	Array.series(5, 0, 3)/5,
	Array.series(5, 0, 4)/5
]);
1 Like

And is there any way to setup the postwindow to always show the true representation of the numbers, so hat I am less confused but checking it?

Why this makes a difference in the output numbers?

SC supports integers and floats - if you write 5 you will get an Integer: 5.class // Integer
if you add a decimal point sclang will represent the number as a Float: 5.0.class //float
If you divide integers, say 5 by 3 ,the result will be a Float: (5 / 3).class //Float

you can display a Float up to an arbitrary number of digits using the method asStringPrec

try (3 / 5).asStringPrec(20) //0.5999999999999999778
notice its a little bit smaller than 0.6
now try (4 / 5).asStringPrec //0.80000000000000004441
its a little bigger than 0.8

When you add many of these numbers the small errors accumulate!

in any case: (3/5)+(3/5)+(3/5)+(3/5) < (4/5)+(4/5)+4/5).

When we evaluate Array.series(5, 0, 3) we are adding good old Integers together - when we divide two equal integers by the same number we will get the same float: (4+4+4/7) == (3+3+3+3/7) // true

so… If you first do all your summing and then divide you will get the expected answer using ==.

a more thorough solution is this extension by @smoge which adds a class for Rational numbers GitHub - smoge/Rational: Rational Numbers extension to SuperCollider (perhaps a good addition to the core library ?)

1 Like

Rational numbers make a lot of sense for music projects. Unfortunately, we can’t set operator precedence in SuperCollider yet, otherwise, we could get rid of all those parentheses.

Quarks.install("Rational");

((3%/5) + (3%/5) + (3%/5) + (3%/5)) == ((4%/5) + (4%/5) + (4%/5));
-> true

Although you can work around it to look less like lisp:

[3%/5, 3%/5, 3%/5, 3%/5].sum == [4%/5, 4%/5, 4%/5].sum;
-> true

In Haskell that could be set with something like infixl 7 %/. In SuperCollider, that would allow:

3%/5 + 3%/5 + 3%/5 + 3%/5 == 4%/5 + 4%/5 + 4%/5

It’s like that by default in Haskell, Ratio (%) has higher precedence than (+) and (==):

ghci> :info (%)
(%) :: Integral a => a -> a -> Ratio a  -- Defined in ‘GHC.Real’
infixl 7 %
ghci> :info (+)
type Num :: * -> Constraint
class Num a where
  (+) :: a -> a -> a
  ...
        -- Defined in ‘GHC.Num’
infixl 6 +
ghci> :info (==)
type Eq :: * -> Constraint
class Eq a where
  (==) :: a -> a -> Bool
  ...
        -- Defined in ‘GHC.Classes’
infix 4 ==

Maybe an idea for SC4.

2 Likes

Another easy-to-remember failure:

sc3> 0.1 + 0.1
-> 0.2
sc3> (0.1 + 0.1) == 0.2
-> true
sc3> 0.1 + 0.1 + 0.1
-> 0.3
sc3> (0.1 + 0.1 + 0.1) == 0.3
-> false
2 Likes

Since decimal representation of binary floats will typically be inaccurate, I’d advise to never rely on float == float
@jamshark70 suggestion is good, though I’d suggest a different method since fuzzyEqual returns a 0.0/1.0 instead of a boolean:

2.4.equalWithPrecision(2.4, 0.0001)

will give you a boolean. You can even skip the precision argument, since it defaults to 0.0001 (2.4.equalWithPrecision(2.4)).

equalWithPrecision everywhere seems to pollute the code a bit too much for me

regarding comments on your proposed fix…

perhaps you’ve seen the discussion on this thread: The Future of SuperCollider Development Efforts

The problem is that our (heroic) maintainers’ efforts are stretched thin - an effort is underway to reorganize and reinvigorate development - perhaps you might jump in when appropriate! It looks like the plan is to devolve some power to groups. Perhaps this will break the (many) logjams…

1 Like

amusing idea: what about defining == as equalWithPrecision for Float ?

I expect it breaks something fundamental but who knows…

Maybe coming up with a new operator would be better

Just for fun, I would n’t do it :slight_smile:

+ Float {

    +=- { |aNumber, precision = 0.0001| ^this.equalWithPrecision(aNumber, precision) }

}

Then


0.24 +=-.(0.01) 0.22 // false
0.24 +=-.(0.1) 0.22 // true
1 Like