From bd1ef76c7938bccc452766bbe17b26c4dfc40d08 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 24 Jul 2022 17:21:21 +0200 Subject: [PATCH 1/4] Adding tests for showing #5647, difference in max/min in debug with floats --- .../FSharp.Core/OperatorsModule2.fs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs index d4197529ea6..b54cd7bed5f 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs @@ -307,6 +307,15 @@ type OperatorsModule2() = // reference type let result = Operators.max "A" "ABC" Assert.AreEqual("ABC", result) + + // floating point + let assertNaN f result = Assert.True(f result, "Operators.max with NaN must result in NaN.") + Operators.max nan 1.0 |> assertNaN Double.IsNaN + Operators.max 1.0 nan |> assertNaN Double.IsNaN + Operators.max nan nan |> assertNaN Double.IsNaN + Operators.max 1.0f nanf |> assertNaN Single.IsNaN + Operators.max nanf 1.0f |> assertNaN Single.IsNaN + Operators.max nanf nanf |> assertNaN Single.IsNaN [] member _.min() = @@ -325,6 +334,15 @@ type OperatorsModule2() = // reference type let result = Operators.min "A" "ABC" Assert.AreEqual("A", result) + + // floating point + let assertNaN f result = Assert.True(f result, "Operators.min with NaN must result in NaN.") + Operators.min nan 1.0 |> assertNaN Double.IsNaN + Operators.min 1.0 nan |> assertNaN Double.IsNaN + Operators.min nan nan |> assertNaN Double.IsNaN + Operators.min 1.0f nanf |> assertNaN Single.IsNaN + Operators.min nanf 1.0f |> assertNaN Single.IsNaN + Operators.min nanf nanf |> assertNaN Single.IsNaN [] member _.nan() = From 2c63c2309614512589a2d6e3e3624b21cf401e82 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 24 Jul 2022 20:21:06 +0200 Subject: [PATCH 2/4] Add a couple of more tests to cover minus/plus zero, negative nan, and infinities --- .../FSharp.Core/OperatorsModule2.fs | 134 +++++++++++++----- 1 file changed, 101 insertions(+), 33 deletions(-) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs index b54cd7bed5f..04575715419 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs @@ -16,6 +16,16 @@ open Xunit #nowarn "3370" +/// floating point helpers for min/max tests +module FP = + let shouldBeNaN f result = Assert.True(f result, "Operators.max with NaN must result in NaN.") + let shouldBeZero (v, test) result = Assert.True(test result, $"Operators.max expected a %s{v} zero.") + let positive = "positive", (Double.IsNegative >> not) + let positivef = "positive", (Single.IsNegative >> not) + let negative = "positive", Double.IsNegative + let negativef = "positive", Single.IsNegative + + /// If this type compiles without error it is correct /// Wrong if you see: FS0670 This code is not sufficiently generic. The type variable ^T could not be generalized because it would escape its scope. type TestFs0670Error<'T> = @@ -292,58 +302,116 @@ type OperatorsModule2() = [] member _.max() = + let shouldEqual a b = Assert.AreEqual(a, b) // value type - let result = Operators.max 10 8 - Assert.AreEqual(10, result) - + Operators.max 10 8 |> shouldEqual 10 + // negative - let result = Operators.max -10.0 -8.0 - Assert.AreEqual(-8.0, result) + Operators.max -10.0M -8.0M |> shouldEqual -8.0M // zero - let result = Operators.max 0 0 - Assert.AreEqual(0, result) - + Operators.max 0 0 |> shouldEqual 0 + // reference type - let result = Operators.max "A" "ABC" - Assert.AreEqual("ABC", result) + Operators.max "A" "ABC" |> shouldEqual "ABC" // floating point - let assertNaN f result = Assert.True(f result, "Operators.max with NaN must result in NaN.") - Operators.max nan 1.0 |> assertNaN Double.IsNaN - Operators.max 1.0 nan |> assertNaN Double.IsNaN - Operators.max nan nan |> assertNaN Double.IsNaN - Operators.max 1.0f nanf |> assertNaN Single.IsNaN - Operators.max nanf 1.0f |> assertNaN Single.IsNaN - Operators.max nanf nanf |> assertNaN Single.IsNaN + // negative zero + Operators.max 0.0 -0.0 |> FP.shouldBeZero FP.positive + Operators.max -0.0 0.0 |> FP.shouldBeZero FP.positive + Operators.max -1.0 0.0 |> FP.shouldBeZero FP.positive + Operators.max -1.0 -0.0 |> FP.shouldBeZero FP.negative + Operators.max 0.0f -0.0f |> FP.shouldBeZero FP.positivef + Operators.max -0.0f 0.0f |> FP.shouldBeZero FP.positivef + Operators.max -1.0f 0.0f |> FP.shouldBeZero FP.positivef + Operators.max -1.0f -0.0f |> FP.shouldBeZero FP.negativef + Operators.max -1.0 -2.0 |> shouldEqual -2.0 + Operators.max infinity -infinity |> shouldEqual infinity + Operators.max infinityf -infinityf |> shouldEqual infinityf + + Operators.max nan 1.0 |> FP.shouldBeNaN Double.IsNaN + Operators.max 1.0 nan |> FP.shouldBeNaN Double.IsNaN + Operators.max nan nan |> FP.shouldBeNaN Double.IsNaN + Operators.max nan -1.0 |> FP.shouldBeNaN Double.IsNaN + Operators.max -1.0 nan |> FP.shouldBeNaN Double.IsNaN + Operators.max nan -nan |> FP.shouldBeNaN Double.IsNaN + Operators.max 1.0f nanf |> FP.shouldBeNaN Single.IsNaN + Operators.max nanf 1.0f |> FP.shouldBeNaN Single.IsNaN + Operators.max nanf nanf |> FP.shouldBeNaN Single.IsNaN + + // no difference in outcome, just positive NaN, when comparing to other types of NaN + let negNaN = Math.CopySign(nan, -1.0) + let negNaNf = MathF.CopySign(nanf, -1.0f) + Operators.max negNaN 1.0 |> FP.shouldBeNaN Double.IsNaN + Operators.max 1.0 negNaN |> FP.shouldBeNaN Double.IsNaN + Operators.max negNaN negNaN |> FP.shouldBeNaN Double.IsNaN + Operators.max negNaN -1.0 |> FP.shouldBeNaN Double.IsNaN + Operators.max -1.0 negNaN |> FP.shouldBeNaN Double.IsNaN + Operators.max nan negNaN |> FP.shouldBeNaN Double.IsNaN + Operators.max negNaN nan |> FP.shouldBeNaN Double.IsNaN + Operators.max 1.0f negNaNf |> FP.shouldBeNaN Single.IsNaN + Operators.max negNaNf 1.0f |> FP.shouldBeNaN Single.IsNaN + Operators.max negNaNf nanf |> FP.shouldBeNaN Single.IsNaN + Operators.max nanf negNaNf |> FP.shouldBeNaN Single.IsNaN + Operators.max negNaNf negNaNf |> FP.shouldBeNaN Single.IsNaN + [] member _.min() = + let shouldEqual a b = Assert.AreEqual(a, b) + // value type - let result = Operators.min 10 8 - Assert.AreEqual(8, result) + Operators.min 10 8 |> shouldEqual 8 // negative - let result = Operators.min -10.0 -8.0 - Assert.AreEqual(-10.0, result) + Operators.min -10.0M -8.0M |> shouldEqual -10M // zero - let result = Operators.min 0 0 - Assert.AreEqual(0, result) + Operators.min 0 0 |> shouldEqual 0 // reference type - let result = Operators.min "A" "ABC" - Assert.AreEqual("A", result) + Operators.min "A" "ABC" |> shouldEqual "A" // floating point - let assertNaN f result = Assert.True(f result, "Operators.min with NaN must result in NaN.") - Operators.min nan 1.0 |> assertNaN Double.IsNaN - Operators.min 1.0 nan |> assertNaN Double.IsNaN - Operators.min nan nan |> assertNaN Double.IsNaN - Operators.min 1.0f nanf |> assertNaN Single.IsNaN - Operators.min nanf 1.0f |> assertNaN Single.IsNaN - Operators.min nanf nanf |> assertNaN Single.IsNaN - + // negative zero + Operators.min 0.0 -0.0 |> FP.shouldBeZero FP.negative + Operators.min -0.0 0.0 |> FP.shouldBeZero FP.negative + Operators.min -1.0 0.0 |> shouldEqual -1.0 + Operators.min 1.0 -0.0 |> FP.shouldBeZero FP.negative + Operators.min 0.0f -0.0f |> FP.shouldBeZero FP.negativef + Operators.min -0.0f 0.0f |> FP.shouldBeZero FP.negativef + Operators.min -1.0f 0.0f |> shouldEqual -1.0 + Operators.min 1.0f -0.0f |> FP.shouldBeZero FP.negativef + Operators.min -1.0 -2.0 |> shouldEqual -1.0 + Operators.min infinity -infinity |> shouldEqual -infinity + Operators.min infinityf -infinityf |> shouldEqual -infinityf + + Operators.min nan 1.0 |> FP.shouldBeNaN Double.IsNaN + Operators.min 1.0 nan |> FP.shouldBeNaN Double.IsNaN + Operators.min nan nan |> FP.shouldBeNaN Double.IsNaN + Operators.min nan -1.0 |> FP.shouldBeNaN Double.IsNaN + Operators.min -1.0 nan |> FP.shouldBeNaN Double.IsNaN + Operators.min nan -nan |> FP.shouldBeNaN Double.IsNaN + Operators.min 1.0f nanf |> FP.shouldBeNaN Single.IsNaN + Operators.min nanf 1.0f |> FP.shouldBeNaN Single.IsNaN + Operators.min nanf nanf |> FP.shouldBeNaN Single.IsNaN + + // no difference in outcome, just positive NaN, when comparing to other types of NaN + let negNaN = Math.CopySign(nan, -1.0) + let negNaNf = MathF.CopySign(nanf, -1.0f) + Operators.min negNaN 1.0 |> FP.shouldBeNaN Double.IsNaN + Operators.min 1.0 negNaN |> FP.shouldBeNaN Double.IsNaN + Operators.min negNaN negNaN |> FP.shouldBeNaN Double.IsNaN + Operators.min negNaN -1.0 |> FP.shouldBeNaN Double.IsNaN + Operators.min -1.0 negNaN |> FP.shouldBeNaN Double.IsNaN + Operators.min nan negNaN |> FP.shouldBeNaN Double.IsNaN + Operators.min negNaN nan |> FP.shouldBeNaN Double.IsNaN + Operators.min 1.0f negNaNf |> FP.shouldBeNaN Single.IsNaN + Operators.min negNaNf 1.0f |> FP.shouldBeNaN Single.IsNaN + Operators.min negNaNf nanf |> FP.shouldBeNaN Single.IsNaN + Operators.min nanf negNaNf |> FP.shouldBeNaN Single.IsNaN + Operators.min negNaNf negNaNf |> FP.shouldBeNaN Single.IsNaN + [] member _.nan() = // value type From 06083e1a983a4cb40bb69214bfd9673f9b7c7fc5 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 24 Jul 2022 21:15:22 +0200 Subject: [PATCH 3/4] Update tests based on new insight that NaN in BCL is by default negative (duh!) and replicate weird Math.Max bug --- .../FSharp.Core/OperatorsModule2.fs | 143 +++++++++--------- 1 file changed, 68 insertions(+), 75 deletions(-) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs index 04575715419..afac8e021ba 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs @@ -18,12 +18,15 @@ open Xunit /// floating point helpers for min/max tests module FP = - let shouldBeNaN f result = Assert.True(f result, "Operators.max with NaN must result in NaN.") - let shouldBeZero (v, test) result = Assert.True(test result, $"Operators.max expected a %s{v} zero.") - let positive = "positive", (Double.IsNegative >> not) - let positivef = "positive", (Single.IsNegative >> not) - let negative = "positive", Double.IsNegative - let negativef = "positive", Single.IsNegative + let shouldBe (v, test) result = Assert.True(test result, $"Operators.max/min expected a %s{v}.") + let positive = "positive zero float", (Double.IsNegative >> not) + let positivef = "positive zero float32", (Single.IsNegative >> not) + let negative = "negative zero float", Double.IsNegative + let negativef = "positive zero float32", Single.IsNegative + let positiveNaN = "positive NaN float", Double.IsNegative + let positiveNaNf = "positive NaN float32", Single.IsNegative + let negativeNaN = "positive NaN float", Double.IsNegative + let negativeNaNf = "positive NaN float32", Single.IsNegative /// If this type compiles without error it is correct @@ -317,44 +320,38 @@ type OperatorsModule2() = // floating point // negative zero - Operators.max 0.0 -0.0 |> FP.shouldBeZero FP.positive - Operators.max -0.0 0.0 |> FP.shouldBeZero FP.positive - Operators.max -1.0 0.0 |> FP.shouldBeZero FP.positive - Operators.max -1.0 -0.0 |> FP.shouldBeZero FP.negative - Operators.max 0.0f -0.0f |> FP.shouldBeZero FP.positivef - Operators.max -0.0f 0.0f |> FP.shouldBeZero FP.positivef - Operators.max -1.0f 0.0f |> FP.shouldBeZero FP.positivef - Operators.max -1.0f -0.0f |> FP.shouldBeZero FP.negativef - Operators.max -1.0 -2.0 |> shouldEqual -2.0 + Operators.max 0.0 -0.0 |> FP.shouldBe FP.positive + Operators.max -0.0 0.0 |> FP.shouldBe FP.positive + Operators.max -1.0 0.0 |> FP.shouldBe FP.positive + Operators.max -1.0 -0.0 |> FP.shouldBe FP.negative + Operators.max 0.0f -0.0f |> FP.shouldBe FP.positivef + Operators.max -0.0f 0.0f |> FP.shouldBe FP.positivef + Operators.max -1.0f 0.0f |> FP.shouldBe FP.positivef + Operators.max -1.0f -0.0f |> FP.shouldBe FP.negativef + Operators.max -1.0 -2.0 |> shouldEqual -1.0 Operators.max infinity -infinity |> shouldEqual infinity Operators.max infinityf -infinityf |> shouldEqual infinityf - Operators.max nan 1.0 |> FP.shouldBeNaN Double.IsNaN - Operators.max 1.0 nan |> FP.shouldBeNaN Double.IsNaN - Operators.max nan nan |> FP.shouldBeNaN Double.IsNaN - Operators.max nan -1.0 |> FP.shouldBeNaN Double.IsNaN - Operators.max -1.0 nan |> FP.shouldBeNaN Double.IsNaN - Operators.max nan -nan |> FP.shouldBeNaN Double.IsNaN - Operators.max 1.0f nanf |> FP.shouldBeNaN Single.IsNaN - Operators.max nanf 1.0f |> FP.shouldBeNaN Single.IsNaN - Operators.max nanf nanf |> FP.shouldBeNaN Single.IsNaN - - // no difference in outcome, just positive NaN, when comparing to other types of NaN - let negNaN = Math.CopySign(nan, -1.0) - let negNaNf = MathF.CopySign(nanf, -1.0f) - Operators.max negNaN 1.0 |> FP.shouldBeNaN Double.IsNaN - Operators.max 1.0 negNaN |> FP.shouldBeNaN Double.IsNaN - Operators.max negNaN negNaN |> FP.shouldBeNaN Double.IsNaN - Operators.max negNaN -1.0 |> FP.shouldBeNaN Double.IsNaN - Operators.max -1.0 negNaN |> FP.shouldBeNaN Double.IsNaN - Operators.max nan negNaN |> FP.shouldBeNaN Double.IsNaN - Operators.max negNaN nan |> FP.shouldBeNaN Double.IsNaN - Operators.max 1.0f negNaNf |> FP.shouldBeNaN Single.IsNaN - Operators.max negNaNf 1.0f |> FP.shouldBeNaN Single.IsNaN - Operators.max negNaNf nanf |> FP.shouldBeNaN Single.IsNaN - Operators.max nanf negNaNf |> FP.shouldBeNaN Single.IsNaN - Operators.max negNaNf negNaNf |> FP.shouldBeNaN Single.IsNaN - + // note that the default nan is negative, using -nan (which does change + // the binary representation), makes nan positive + let posNan, negNan, posNanf, negNanf = -nan, nan, -nanf, nanf + Operators.max negNan 1.0 |> FP.shouldBe FP.negativeNaN + Operators.max 1.0 negNan |> FP.shouldBe FP.negativeNaN + Operators.max negNan -1.0 |> FP.shouldBe FP.negativeNaN + Operators.max -1.0 negNan |> FP.shouldBe FP.negativeNaN + Operators.max 1.0f negNanf |> FP.shouldBe FP.negativeNaNf + Operators.max negNanf 1.0f |> FP.shouldBe FP.negativeNaNf + + // truth table + Operators.max negNan negNan |> FP.shouldBe FP.negativeNaN + Operators.max negNan posNan |> FP.shouldBe FP.negativeNaN // Bug in BCL: Math.Max returns first arg if it is any NaN + Operators.max posNan negNan |> FP.shouldBe FP.positiveNaN // Bug in BCL: Math.Max returns first arg if it is any NaN + Operators.max posNan posNan |> FP.shouldBe FP.positiveNaN + + Operators.max negNanf negNanf |> FP.shouldBe FP.negativeNaNf + Operators.max negNanf posNanf |> FP.shouldBe FP.negativeNaNf // Bug in BCL: Math.Max returns first arg if it is any NaN + Operators.max posNanf negNanf |> FP.shouldBe FP.positiveNaNf // Bug in BCL: Math.Max returns first arg if it is any NaN + Operators.max posNanf posNanf |> FP.shouldBe FP.negativeNaNf [] member _.min() = @@ -374,43 +371,39 @@ type OperatorsModule2() = // floating point // negative zero - Operators.min 0.0 -0.0 |> FP.shouldBeZero FP.negative - Operators.min -0.0 0.0 |> FP.shouldBeZero FP.negative - Operators.min -1.0 0.0 |> shouldEqual -1.0 - Operators.min 1.0 -0.0 |> FP.shouldBeZero FP.negative - Operators.min 0.0f -0.0f |> FP.shouldBeZero FP.negativef - Operators.min -0.0f 0.0f |> FP.shouldBeZero FP.negativef - Operators.min -1.0f 0.0f |> shouldEqual -1.0 - Operators.min 1.0f -0.0f |> FP.shouldBeZero FP.negativef - Operators.min -1.0 -2.0 |> shouldEqual -1.0 + Operators.min 0.0 -0.0 |> FP.shouldBe FP.negative + Operators.min -0.0 0.0 |> FP.shouldBe FP.negative + Operators.min 1.0 0.0 |> FP.shouldBe FP.positive + Operators.min 1.0 -0.0 |> FP.shouldBe FP.negative + Operators.min 0.0f -0.0f |> FP.shouldBe FP.negativef + Operators.min -0.0f 0.0f |> FP.shouldBe FP.negativef + Operators.min 1.0f 0.0f |> FP.shouldBe FP.positivef + Operators.min 1.0f -0.0f |> FP.shouldBe FP.negativef + Operators.min -1.0 -2.0 |> shouldEqual -2.0 Operators.min infinity -infinity |> shouldEqual -infinity Operators.min infinityf -infinityf |> shouldEqual -infinityf - Operators.min nan 1.0 |> FP.shouldBeNaN Double.IsNaN - Operators.min 1.0 nan |> FP.shouldBeNaN Double.IsNaN - Operators.min nan nan |> FP.shouldBeNaN Double.IsNaN - Operators.min nan -1.0 |> FP.shouldBeNaN Double.IsNaN - Operators.min -1.0 nan |> FP.shouldBeNaN Double.IsNaN - Operators.min nan -nan |> FP.shouldBeNaN Double.IsNaN - Operators.min 1.0f nanf |> FP.shouldBeNaN Single.IsNaN - Operators.min nanf 1.0f |> FP.shouldBeNaN Single.IsNaN - Operators.min nanf nanf |> FP.shouldBeNaN Single.IsNaN - - // no difference in outcome, just positive NaN, when comparing to other types of NaN - let negNaN = Math.CopySign(nan, -1.0) - let negNaNf = MathF.CopySign(nanf, -1.0f) - Operators.min negNaN 1.0 |> FP.shouldBeNaN Double.IsNaN - Operators.min 1.0 negNaN |> FP.shouldBeNaN Double.IsNaN - Operators.min negNaN negNaN |> FP.shouldBeNaN Double.IsNaN - Operators.min negNaN -1.0 |> FP.shouldBeNaN Double.IsNaN - Operators.min -1.0 negNaN |> FP.shouldBeNaN Double.IsNaN - Operators.min nan negNaN |> FP.shouldBeNaN Double.IsNaN - Operators.min negNaN nan |> FP.shouldBeNaN Double.IsNaN - Operators.min 1.0f negNaNf |> FP.shouldBeNaN Single.IsNaN - Operators.min negNaNf 1.0f |> FP.shouldBeNaN Single.IsNaN - Operators.min negNaNf nanf |> FP.shouldBeNaN Single.IsNaN - Operators.min nanf negNaNf |> FP.shouldBeNaN Single.IsNaN - Operators.min negNaNf negNaNf |> FP.shouldBeNaN Single.IsNaN + // note that the default nan is negative, using -nan (which does change + // the binary representation), makes nan positive + let posNan, negNan, posNanf, negNanf = -nan, nan, -nanf, nanf + Operators.min negNan 1.0 |> FP.shouldBe FP.negativeNaN + Operators.min 1.0 negNan |> FP.shouldBe FP.negativeNaN + Operators.min negNan -1.0 |> FP.shouldBe FP.negativeNaN + Operators.min -1.0 negNan |> FP.shouldBe FP.negativeNaN + Operators.min 1.0f negNanf |> FP.shouldBe FP.negativeNaNf + Operators.min negNanf 1.0f |> FP.shouldBe FP.negativeNaNf + + // truth table + Operators.min negNan negNan |> FP.shouldBe FP.negativeNaN + Operators.min negNan posNan |> FP.shouldBe FP.negativeNaN // Math.Min works like IEEE totalOrder + Operators.min posNan negNan |> FP.shouldBe FP.negativeNaN // Math.Min works like IEEE totalOrder + Operators.min posNan posNan |> FP.shouldBe FP.positiveNaN + + Operators.min negNanf negNanf |> FP.shouldBe FP.negativeNaNf + Operators.min negNanf posNanf |> FP.shouldBe FP.negativeNaNf // Math.Min works like IEEE totalOrder + Operators.min posNanf negNanf |> FP.shouldBe FP.negativeNaNf // Math.Min works like IEEE totalOrder + Operators.min posNanf posNanf |> FP.shouldBe FP.positiveNaNf + [] member _.nan() = From 0b9f15d17b4d762e3b0f3b7f119c33d4b471367d Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Mon, 25 Jul 2022 01:44:40 +0200 Subject: [PATCH 4/4] Fix use of NetStandard 2.1 code and replace by hand-written replacements, Double.IsNegative & SingleToInt32Bits was not available --- .../FSharp.Core/OperatorsModule2.fs | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs index afac8e021ba..38141764ce8 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/OperatorsModule2.fs @@ -16,17 +16,31 @@ open Xunit #nowarn "3370" -/// floating point helpers for min/max tests +/// floating point helpers for min/max tests, because Double.IsNegative and some other methods aren't available yet module FP = + let (<&>) f g a = f a && g a + let isNegative x = + // From https://github.com/dotnet/runtime/blob/a5f3676cc71e176084f0f7f1f6beeecd86fbeafc/src/libraries/System.Private.CoreLib/src/System/Double.cs#L86 + let signMask = 0x8000_0000_0000_0000L + let intValue = BitConverter.DoubleToInt64Bits x + intValue &&& signMask = signMask + + let isNegativef (x: float32) = + // https://github.com/dotnet/runtime/blob/a5f3676cc71e176084f0f7f1f6beeecd86fbeafc/src/libraries/System.Private.CoreLib/src/System/Single.cs#L85 + let signMask = 0x8000_0000_0000_0000L + let intValue = x |> float |> BitConverter.DoubleToInt64Bits // cannot use SingleToInt32Bits yet... + intValue &&& signMask = signMask + let shouldBe (v, test) result = Assert.True(test result, $"Operators.max/min expected a %s{v}.") - let positive = "positive zero float", (Double.IsNegative >> not) - let positivef = "positive zero float32", (Single.IsNegative >> not) - let negative = "negative zero float", Double.IsNegative - let negativef = "positive zero float32", Single.IsNegative - let positiveNaN = "positive NaN float", Double.IsNegative - let positiveNaNf = "positive NaN float32", Single.IsNegative - let negativeNaN = "positive NaN float", Double.IsNegative - let negativeNaNf = "positive NaN float32", Single.IsNegative + let positive = "positive zero float", (isNegative >> not) + let positivef = "positive zero float32", (isNegativef >> not) + let negative = "negative zero float", isNegative + let negativef = "positive zero float32", isNegativef + let positiveNaN = "positive NaN float", (isNegative >> not) <&> Double.IsNaN + let positiveNaNf = "positive NaN float32", (isNegativef >> not) <&> Single.IsNaN + let negativeNaN = "positive NaN float", isNegative <&> Double.IsNaN + let negativeNaNf = "positive NaN float32", isNegativef <&> Single.IsNaN + let f() = isNegativef <&> Single.IsNaN /// If this type compiles without error it is correct @@ -42,6 +56,28 @@ type TestFs0670Error<'T> = type OperatorsModule2() = + [] + member _.isNegativeHelpers() = + let shouldBeTrueFor test = Assert.True(FP.isNegative test, $"Helper FP.isNegative should return true for %f{test}") + let shouldBeFalseFor test = Assert.False(FP.isNegative test, $"Helper FP.isNegative should return false for %f{test}") + + [ -1.0; nan (* default nan is negative! *); -0.0; -9999.99999; -infinity; -Double.Epsilon] + |> List.iter shouldBeTrueFor + + [ 1.0; -nan (* default nan is negative! *); 0.0; 9999.99999; infinity; Double.Epsilon] + |> List.iter shouldBeFalseFor + + [] + member _.isNegativefHelper() = + let shouldBeTrueFor test = Assert.True(FP.isNegativef test, $"Helper FP.isNegativef should return true for %f{test}") + let shouldBeFalseFor test = Assert.False(FP.isNegativef test, $"Helper FP.isNegativef should return false for %f{test}") + + [ -1.0f; nanf (* default nan is negative! *); -0.0f; -9999.99999f; -infinityf; -Single.Epsilon] + |> List.iter shouldBeTrueFor + + [ 1.0f; -nanf (* default nan is negative! *); 0.0f; 9999.99999f; infinityf; Single.Epsilon] + |> List.iter shouldBeFalseFor + [] member _.int() = // int