From dbc9b1a85680d9a15f51617f4d20ed5757aa2fdf Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Sun, 30 Aug 2020 11:08:59 -0700 Subject: [PATCH] lerpDouble: stricter handling of NaN and infinity (#20871) Previously, the behaviour of lerpDouble with respect to NaN and infinity was relatively complex and difficult to reason about. This patch simplifies the behaviour with respect to those conditions and adds documentation and tests. In general, if `a == b` or both values are null, infinite, or NaN, `a` is returned. Otherwise we require `a` and `b` and `t` to be finite or null and the result of the linear interpolation is returned. --- lib/ui/lerp.dart | 16 +++++--- testing/dart/lerp_test.dart | 76 ++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/lib/ui/lerp.dart b/lib/ui/lerp.dart index 91a04c2cb..c4841e2ae 100644 --- a/lib/ui/lerp.dart +++ b/lib/ui/lerp.dart @@ -6,14 +6,20 @@ part of dart.ui; -/// Linearly interpolate between two numbers. -// TODO(cbracken): Consider making a and b non-nullable. -// https://github.com/flutter/flutter/issues/64617 +/// Linearly interpolate between two numbers, `a` and `b`, by an extrapolation +/// factor `t`. +/// +/// When `a` and `b` are equal or both NaN, `a` is returned. Otherwise, if +/// `a`, `b`, and `t` are required to be finite or null, and the result of `a + +/// (b - a) * t` is returned, where nulls are defaulted to 0.0. double? lerpDouble(num? a, num? b, double t) { - if (a == null && b == null) - return null; + if (a == b || (a?.isNaN == true) && (b?.isNaN == true)) + return a?.toDouble(); a ??= 0.0; b ??= 0.0; + assert(a.isFinite, 'Cannot interpolate between finite and non-finite values'); + assert(b.isFinite, 'Cannot interpolate between finite and non-finite values'); + assert(t.isFinite, 't must be finite when interpolating between values'); return a + (b - a) * t as double; } diff --git a/testing/dart/lerp_test.dart b/testing/dart/lerp_test.dart index 26ad7b89b..4395e9747 100644 --- a/testing/dart/lerp_test.dart +++ b/testing/dart/lerp_test.dart @@ -7,6 +7,8 @@ import 'dart:ui'; import 'package:test/test.dart'; +import 'test_util.dart'; + void main() { test('lerpDouble should return null if and only if both inputs are null', () { expect(lerpDouble(null, null, 1.0), isNull); @@ -65,47 +67,53 @@ void main() { expect(lerpDouble(10, 0, 5), -40); }); - test('lerpDouble should return NaN if any input is NaN', () { - expect(lerpDouble(0.0, 10.0, double.nan), isNaN); - expect(lerpDouble(0.0, double.infinity, double.nan), isNaN); - expect(lerpDouble(0.0, double.nan, 5.0), isNaN); - expect(lerpDouble(0.0, double.nan, double.infinity), isNaN); - expect(lerpDouble(0.0, double.nan, double.nan), isNaN); - expect(lerpDouble(double.infinity, 10.0, double.nan), isNaN); - expect(lerpDouble(double.infinity, double.infinity, double.nan), isNaN); - expect(lerpDouble(double.infinity, double.nan, 5.0), isNaN); - expect(lerpDouble(double.infinity, double.nan, double.infinity), isNaN); - expect(lerpDouble(double.infinity, double.nan, double.nan), isNaN); - expect(lerpDouble(double.nan, 10.0, 5.0), isNaN); - expect(lerpDouble(double.nan, 10.0, double.infinity), isNaN); - expect(lerpDouble(double.nan, 10.0, double.nan), isNaN); - expect(lerpDouble(double.nan, double.infinity, 5.0), isNaN); - expect(lerpDouble(double.nan, double.infinity, double.infinity), isNaN); - expect(lerpDouble(double.nan, double.infinity, double.nan), isNaN); + test('lerpDouble should return input value in all cases if begin/end are equal', () { + expect(lerpDouble(10.0, 10.0, 5.0), 10.0); + expect(lerpDouble(10.0, 10.0, double.nan), 10.0); + expect(lerpDouble(10.0, 10.0, double.infinity), 10.0); + expect(lerpDouble(10.0, 10.0, -double.infinity), 10.0); + + expect(lerpDouble(10, 10, 5.0), 10.0); + expect(lerpDouble(10, 10, double.nan), 10.0); + expect(lerpDouble(10, 10, double.infinity), 10.0); + expect(lerpDouble(10, 10, -double.infinity), 10.0); + expect(lerpDouble(double.nan, double.nan, 5.0), isNaN); - expect(lerpDouble(double.nan, double.nan, double.infinity), isNaN); expect(lerpDouble(double.nan, double.nan, double.nan), isNaN); + expect(lerpDouble(double.nan, double.nan, double.infinity), isNaN); + expect(lerpDouble(double.nan, double.nan, -double.infinity), isNaN); + + expect(lerpDouble(double.infinity, double.infinity, 5.0), double.infinity); + expect(lerpDouble(double.infinity, double.infinity, double.nan), double.infinity); + expect(lerpDouble(double.infinity, double.infinity, double.infinity), double.infinity); + expect(lerpDouble(double.infinity, double.infinity, -double.infinity), double.infinity); + + expect(lerpDouble(-double.infinity, -double.infinity, 5.0), -double.infinity); + expect(lerpDouble(-double.infinity, -double.infinity, double.nan), -double.infinity); + expect(lerpDouble(-double.infinity, -double.infinity, double.infinity), -double.infinity); + expect(lerpDouble(-double.infinity, -double.infinity, -double.infinity), -double.infinity); }); - test('lerpDouble returns NaN if interpolation results in Infinity - Infinity', () { - expect(lerpDouble(double.infinity, 10.0, 5.0), isNaN); - expect(lerpDouble(double.infinity, 10.0, double.infinity), isNaN); - expect(lerpDouble(-double.infinity, 10.0, 5.0), isNaN); - expect(lerpDouble(-double.infinity, 10.0, double.infinity), isNaN); + test('lerpDouble should throw AssertionError if interpolation value is NaN and a != b', () { + expectAssertion(() => lerpDouble(0.0, 10.0, double.nan)); }); - test('lerpDouble returns +/- infinity if interpolating towards an infinity', () { - expect(lerpDouble(double.infinity, 10.0, -5.0)?.isInfinite, isTrue); - expect(lerpDouble(double.infinity, 10.0, -double.infinity)?.isInfinite, isTrue); - expect(lerpDouble(-double.infinity, 10.0, -5.0)?.isInfinite, isTrue); - expect(lerpDouble(-double.infinity, 10.0, -double.infinity)?.isInfinite, isTrue); - expect(lerpDouble(0.0, double.infinity, 5.0)?.isInfinite, isTrue); - expect(lerpDouble(0.0, double.infinity, -5.0)?.isInfinite, isTrue); - expect(lerpDouble(0.0, 10.0, double.infinity)?.isInfinite, isTrue); - expect(lerpDouble(0.0, double.infinity, double.infinity)?.isInfinite, isTrue); + test('lerpDouble should throw AssertionError if interpolation value is +/- infinity and a != b', () { + expectAssertion(() => lerpDouble(0.0, 10.0, double.infinity)); + expectAssertion(() => lerpDouble(0.0, 10.0, -double.infinity)); }); - test('lerpDouble returns NaN if start/end and interpolation value are infinity', () { - expect(lerpDouble(double.infinity, double.infinity, double.infinity), isNaN); + test('lerpDouble should throw AssertionError if either start or end are NaN', () { + expectAssertion(() => lerpDouble(double.nan, 10.0, 5.0)); + expectAssertion(() => lerpDouble(0.0, double.nan, 5.0)); + }); + + test('lerpDouble should throw AssertionError if either start or end are +/- infinity', () { + expectAssertion(() => lerpDouble(double.infinity, 10.0, 5.0)); + expectAssertion(() => lerpDouble(-double.infinity, 10.0, 5.0)); + expectAssertion(() => lerpDouble(0.0, double.infinity, 5.0)); + expectAssertion(() => lerpDouble(0.0, -double.infinity, 5.0)); }); } + +final Matcher throwsAssertionError = throwsA(const TypeMatcher()); -- GitLab