From 4c2e853aef40440cba67f8b3bed67daf36036886 Mon Sep 17 00:00:00 2001 From: Ian Mackenzie Date: Mon, 25 Mar 2024 22:48:12 -0400 Subject: [PATCH 1/5] Add Float.Extra.[equalWithin,interpolateFrom] --- src/Float/Extra.elm | 65 ++++++++++++++++++++++++++++++++++++++++++-- tests/FloatTests.elm | 62 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/src/Float/Extra.elm b/src/Float/Extra.elm index 3605d1a..faf0554 100644 --- a/src/Float/Extra.elm +++ b/src/Float/Extra.elm @@ -1,8 +1,9 @@ module Float.Extra exposing - ( aboutEqual + ( aboutEqual, equalWithin , toFixedDecimalPlaces, toFixedSignificantDigits, boundaryValuesAsUnicode , range , modBy + , interpolateFrom ) {-| Convenience functions for dealing with Floats. @@ -10,7 +11,7 @@ module Float.Extra exposing # Equality -@docs aboutEqual +@docs aboutEqual, equalWithin # Formatting Floats @@ -27,6 +28,11 @@ module Float.Extra exposing @docs modBy + +# Interpolation + +@docs interpolateFrom + -} -- toFixedDecimalDigits implementation @@ -310,7 +316,7 @@ boundaryValuesAsUnicode formatter value = --- aboutEqual +-- Equality {-| Comparing Floats with `==` is usually wrong, unless you basically care for reference equality, since floating point @@ -343,6 +349,20 @@ aboutEqual a b = abs (a - b) <= 1.0e-5 + 1.0e-8 * abs a +{-| Check if two values are equal within a given tolerance. + + Float.Extra.equalWithin 1e-6 1.9999 2.0001 + --> False + + Float.Extra.equalWithin 1e-3 1.9999 2.0001 + --> True + +-} +equalWithin : Float -> Float -> Float -> Bool +equalWithin tolerance firstValue secondValue = + abs (secondValue - firstValue) <= tolerance + + -- Range @@ -408,3 +428,42 @@ in `Float.Extra.modBy modulus x`. modBy : Float -> Float -> Float modBy modulus x = x - modulus * toFloat (floor (x / modulus)) + + +{-| Interpolate from the first value to the second, based on a parameter that +ranges from zero to one. Passing a parameter value of zero will return the start +value and passing a parameter value of one will return the end value. + + Float.Extra.interpolateFrom 5 10 0 + --> 5 + + Float.Extra.interpolateFrom 5 10 1 + --> 10 + + Float.Extra.interpolateFrom 5 10 0.6 + --> 8 + +The end value can be less than the start value: + + Float.Extra.interpolateFrom 10 5 0.1 + --> 9.5 + +Parameter values less than zero or greater than one can be used to extrapolate: + + Float.Extra.interpolateFrom 5 10 1.5 + --> 12.5 + + Float.Extra.interpolateFrom 5 10 -0.5 + --> 2.5 + + Float.Extra.interpolateFrom 10 5 -0.2 + --> 11 + +-} +interpolateFrom : Float -> Float -> Float -> Float +interpolateFrom start end parameter = + if parameter <= 0.5 then + start + parameter * (end - start) + + else + end + (1 - parameter) * (start - end) diff --git a/tests/FloatTests.elm b/tests/FloatTests.elm index 8383e0f..6778da9 100644 --- a/tests/FloatTests.elm +++ b/tests/FloatTests.elm @@ -1,6 +1,15 @@ -module FloatTests exposing (modByTests, testAboutEqual, testBoundaryValuesAsUnicode, testRange, testToFixedDecimalPlaces, testToFixedSignificantDigits) +module FloatTests exposing + ( modByTests + , testAboutEqual + , testBoundaryValuesAsUnicode + , testEqualWithin + , testInterpolateFrom + , testRange + , testToFixedDecimalPlaces + , testToFixedSignificantDigits + ) -import Expect exposing (FloatingPointTolerance(..)) +import Expect exposing (Expectation, FloatingPointTolerance(..)) import Float.Extra import Fuzz exposing (Fuzzer) import List.Extra exposing (Step(..)) @@ -214,6 +223,32 @@ testAboutEqual = ] +testEqualWithin : Test +testEqualWithin = + describe "equalWithin should compare numbers as equal within a given tolerance" + [ test "small positive error" <| + \() -> + Float.Extra.equalWithin 0.01 1 1.001 + |> Expect.equal True + , test "small negative error" <| + \() -> + Float.Extra.equalWithin 0.01 1 0.999 + |> Expect.equal True + , test "large positive error" <| + \() -> + Float.Extra.equalWithin 0.01 1 1.1 + |> Expect.equal False + , test "large negative error" <| + \() -> + Float.Extra.equalWithin 0.01 1 0.9 + |> Expect.equal False + , test "infinity not equal to itself within any finite tolerance" <| + \() -> + Float.Extra.equalWithin 0.01 (1 / 0) (1 / 0) + |> Expect.equal False + ] + + testRange : Test testRange = describe "range start stop step" @@ -364,3 +399,26 @@ modByTests = Float.Extra.modBy (toFloat a) (toFloat b) |> Expect.within (Absolute 1.0e-20) (toFloat (modBy a b)) ] + + +testInterpolateFrom : Test +testInterpolateFrom = + describe "interpolateFrom" + [ fuzz2 Fuzz.niceFloat Fuzz.niceFloat "should return the start value exactly if given 0" <| + \a b -> Float.Extra.interpolateFrom a b 0 |> expectExactly a + , fuzz2 Fuzz.niceFloat Fuzz.niceFloat "should return the end value exactly if given 1" <| + \a b -> Float.Extra.interpolateFrom a b 1 |> expectExactly b + , fuzz2 Fuzz.niceFloat Fuzz.niceFloat "should return the mean if given 0.5" <| + \a b -> + Float.Extra.interpolateFrom a b 0.5 + |> Float.Extra.aboutEqual ((a + b) / 2) + |> Expect.equal True + ] + + +{-| In some cases (like above in testInterpolateFrom) +you _do_ actually want to check two floating-point numbers for equality +-} +expectExactly : Float -> Float -> Expectation +expectExactly expected actual = + actual |> Expect.within (Expect.Absolute 0.0) expected From 0d589570c6786a5923b2777d85d831adcabd6be1 Mon Sep 17 00:00:00 2001 From: Ian Mackenzie Date: Mon, 25 Mar 2024 22:59:13 -0400 Subject: [PATCH 2/5] Reformat examples Work around current limitations in elm-verify-examples (see https://github.com/stoeffel/elm-verify-examples/issues/83) --- src/Float/Extra.elm | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/Float/Extra.elm b/src/Float/Extra.elm index faf0554..fc12076 100644 --- a/src/Float/Extra.elm +++ b/src/Float/Extra.elm @@ -351,11 +351,9 @@ aboutEqual a b = {-| Check if two values are equal within a given tolerance. - Float.Extra.equalWithin 1e-6 1.9999 2.0001 - --> False + Float.Extra.equalWithin 1.0e-6 1.9999 2.0001 --> False - Float.Extra.equalWithin 1e-3 1.9999 2.0001 - --> True + Float.Extra.equalWithin 1.0e-3 1.9999 2.0001 --> True -} equalWithin : Float -> Float -> Float -> Bool @@ -434,30 +432,23 @@ modBy modulus x = ranges from zero to one. Passing a parameter value of zero will return the start value and passing a parameter value of one will return the end value. - Float.Extra.interpolateFrom 5 10 0 - --> 5 + Float.Extra.interpolateFrom 5 10 0 --> 5 - Float.Extra.interpolateFrom 5 10 1 - --> 10 + Float.Extra.interpolateFrom 5 10 1 --> 10 - Float.Extra.interpolateFrom 5 10 0.6 - --> 8 + Float.Extra.interpolateFrom 5 10 0.6 --> 8 The end value can be less than the start value: - Float.Extra.interpolateFrom 10 5 0.1 - --> 9.5 + Float.Extra.interpolateFrom 10 5 0.1 --> 9.5 Parameter values less than zero or greater than one can be used to extrapolate: - Float.Extra.interpolateFrom 5 10 1.5 - --> 12.5 + Float.Extra.interpolateFrom 5 10 1.5 --> 12.5 - Float.Extra.interpolateFrom 5 10 -0.5 - --> 2.5 + Float.Extra.interpolateFrom 5 10 -0.5 --> 2.5 - Float.Extra.interpolateFrom 10 5 -0.2 - --> 11 + Float.Extra.interpolateFrom 10 5 -0.2 --> 11 -} interpolateFrom : Float -> Float -> Float -> Float From 4df9c0fcfbccd281f7212f5113c67b8793cfb70f Mon Sep 17 00:00:00 2001 From: Ian Mackenzie Date: Mon, 25 Mar 2024 23:31:00 -0400 Subject: [PATCH 3/5] Reformat examples again --- src/Float/Extra.elm | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Float/Extra.elm b/src/Float/Extra.elm index fc12076..64e5468 100644 --- a/src/Float/Extra.elm +++ b/src/Float/Extra.elm @@ -351,9 +351,11 @@ aboutEqual a b = {-| Check if two values are equal within a given tolerance. - Float.Extra.equalWithin 1.0e-6 1.9999 2.0001 --> False + Float.Extra.equalWithin 1.0e-6 1.9999 2.0001 + --> False - Float.Extra.equalWithin 1.0e-3 1.9999 2.0001 --> True + Float.Extra.equalWithin 1.0e-3 1.9999 2.0001 + --> True -} equalWithin : Float -> Float -> Float -> Bool @@ -432,23 +434,23 @@ modBy modulus x = ranges from zero to one. Passing a parameter value of zero will return the start value and passing a parameter value of one will return the end value. - Float.Extra.interpolateFrom 5 10 0 --> 5 + Float.Extra.interpolateFrom 5 10 0 == 5 - Float.Extra.interpolateFrom 5 10 1 --> 10 + Float.Extra.interpolateFrom 5 10 1 == 10 - Float.Extra.interpolateFrom 5 10 0.6 --> 8 + Float.Extra.interpolateFrom 5 10 0.6 == 8 The end value can be less than the start value: - Float.Extra.interpolateFrom 10 5 0.1 --> 9.5 + Float.Extra.interpolateFrom 10 5 0.1 == 9.5 Parameter values less than zero or greater than one can be used to extrapolate: - Float.Extra.interpolateFrom 5 10 1.5 --> 12.5 + Float.Extra.interpolateFrom 5 10 1.5 == 12.5 - Float.Extra.interpolateFrom 5 10 -0.5 --> 2.5 + Float.Extra.interpolateFrom 5 10 -0.5 == 2.5 - Float.Extra.interpolateFrom 10 5 -0.2 --> 11 + Float.Extra.interpolateFrom 10 5 -0.2 == 11 -} interpolateFrom : Float -> Float -> Float -> Float From d795f718e4eaf781f8a17b2428ccb391c4a062a3 Mon Sep 17 00:00:00 2001 From: Ian Mackenzie Date: Tue, 26 Mar 2024 23:09:23 -0400 Subject: [PATCH 4/5] Have equalWithin return true for same-sign infinities Consistent with aboutEqual, although I still think it feels weird --- src/Float/Extra.elm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Float/Extra.elm b/src/Float/Extra.elm index 64e5468..4d1ad80 100644 --- a/src/Float/Extra.elm +++ b/src/Float/Extra.elm @@ -360,7 +360,11 @@ aboutEqual a b = -} equalWithin : Float -> Float -> Float -> Bool equalWithin tolerance firstValue secondValue = - abs (secondValue - firstValue) <= tolerance + if isInfinite firstValue || isInfinite secondValue then + firstValue == secondValue + + else + abs (secondValue - firstValue) <= tolerance From 15a35873eb69c521c819e00b8938ca00fd8adedc Mon Sep 17 00:00:00 2001 From: Ian Mackenzie Date: Mon, 15 Apr 2024 11:04:22 -0400 Subject: [PATCH 5/5] Simplify Float.Extra.interpolateFrom Better performance at the cost of a small amount of numerical stability --- src/Float/Extra.elm | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Float/Extra.elm b/src/Float/Extra.elm index 4d1ad80..d763ba7 100644 --- a/src/Float/Extra.elm +++ b/src/Float/Extra.elm @@ -456,11 +456,12 @@ Parameter values less than zero or greater than one can be used to extrapolate: Float.Extra.interpolateFrom 10 5 -0.2 == 11 +Note that in some cases, due to numerical roundoff, passing a parameter value of 1.0 +may not return _exactly_ the end value, e.g. + + Float.Extra.interpolateFrom 1.0e6 1.0e-6 1.0 == 1.00000761449337e-6 + -} interpolateFrom : Float -> Float -> Float -> Float interpolateFrom start end parameter = - if parameter <= 0.5 then - start + parameter * (end - start) - - else - end + (1 - parameter) * (start - end) + start + parameter * (end - start)