Skip to content

Commit

Permalink
Add double.nextUp, double.nextDown, double.nextTowards(double),…
Browse files Browse the repository at this point in the history
… and `double.ulp`.
  • Loading branch information
renggli committed Oct 28, 2023
1 parent 6fcf0d6 commit c6d512a
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/math.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export 'src/math/binomial.dart'
export 'src/math/bit.dart' show BitUint32Extension;
export 'src/math/digits.dart'
show DigitsBigIntExtension, DigitsIntegerExtension;
export 'src/math/double.dart' show DoubleExtension;
export 'src/math/factorial.dart'
show FactorialBigIntExtension, FactorialIntegerExtension;
export 'src/math/hyperbolic.dart' show HyperbolicNumberExtension;
Expand Down
59 changes: 59 additions & 0 deletions lib/src/math/double.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'dart:typed_data';

extension DoubleExtension on double {
/// Returns the nearest [double] in direction of positive infinity.
double get nextUp {
if (isNaN || this == double.infinity) return this;
if (this == 0.0) return double.minPositive;
return this < 0.0 ? _decrement(this) : _increment(this);
}

/// Returns the nearest [double] in direction of negative infinity.
double get nextDown {
if (isNaN || this == double.negativeInfinity) return this;
if (this == 0.0) return -double.minPositive;
return this < 0.0 ? _increment(this) : _decrement(this);
}

/// Returns the url (unit in the last place) of this value, that is the
/// positive spacing between two consecutive floating-point numbers.
double get ulp {
if (this < 0.0) return (-this).ulp;
if (!isFinite) return this;
if (this == double.maxFinite) return this - nextDown;
return nextUp - this;
}

/// Returns the nearest [double] in direction of [other]. If this is identical
/// to [other], other is returned.
double nextTowards(double other) {
if (isNaN || other.isNaN) return double.nan;
if (this == other) return other;
if (this < other) return nextUp;
return nextDown;
}
}

final _dataBuffer = ByteData(8);

double _increment(double value) {
_dataBuffer.setFloat64(0, value);
final hi32 = _dataBuffer.getUint32(0);
final lo32 = _dataBuffer.getUint32(4);
_dataBuffer.setUint32(4, lo32 + 1);
if (lo32 == 0xffffffff) {
_dataBuffer.setUint32(0, hi32 + 1);
}
return _dataBuffer.getFloat64(0);
}

double _decrement(double value) {
_dataBuffer.setFloat64(0, value);
final hi32 = _dataBuffer.getUint32(0);
final lo32 = _dataBuffer.getUint32(4);
_dataBuffer.setUint32(4, lo32 - 1);
if (lo32 == 0x00000000) {
_dataBuffer.setUint32(0, hi32 - 1);
}
return _dataBuffer.getFloat64(0);
}
132 changes: 132 additions & 0 deletions test/math_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// ignore_for_file: deprecated_member_use_from_same_package

import 'dart:math' as math;
import 'dart:math';

import 'package:more/collection.dart';
import 'package:more/math.dart';
Expand Down Expand Up @@ -123,6 +124,137 @@ void main() {
}
});
});
group('double', () {
group('nextDown', () {
double nextDown(double value) {
final result = value.nextDown;
if (value.isNaN) {
expect(result, isNaN);
} else if (value == double.negativeInfinity) {
expect(result, double.negativeInfinity);
} else {
expect(result, lessThan(value));
expect(result.nextUp, value);
}
return result;
}

test('basic', () {
expect(nextDown(-1.0), -1.0000000000000002);
expect(nextDown(1.0), 0.9999999999999999);
});
test('special', () {
expect(nextDown(double.nan), isNaN);
expect(nextDown(double.infinity), double.maxFinite);
expect(nextDown(double.negativeInfinity), double.negativeInfinity);
expect(nextDown(-double.maxFinite), double.negativeInfinity);
expect(nextDown(double.minPositive), 0.0);
expect(nextDown(0.0), -double.minPositive);
});
test('stress', () {
final random = Random(4678);
for (var i = 0; i < 1000; i++) {
nextDown(2 *
(random.nextDouble() - 0.5) *
pow(10, random.nextInt(51) - 25));
}
});
});
group('nextUp', () {
double nextUp(double value) {
final result = value.nextUp;
if (value.isNaN) {
expect(result, isNaN);
} else if (value == double.infinity) {
expect(result, double.infinity);
} else {
expect(result, greaterThan(value));
expect(result.nextDown, value);
}
return result;
}

test('basic', () {
expect(nextUp(-1.0), -0.9999999999999999);
expect(nextUp(1.0), 1.0000000000000002);
});
test('special', () {
expect(nextUp(double.nan), isNaN);
expect(nextUp(double.infinity), double.infinity);
expect(nextUp(double.negativeInfinity), -double.maxFinite);
expect(nextUp(double.maxFinite), double.infinity);
expect(nextUp(-double.minPositive), 0.0);
expect(nextUp(0.0), double.minPositive);
});
test('stress', () {
final random = Random(8913);
for (var i = 0; i < 1000; i++) {
nextUp(2 *
(random.nextDouble() - 0.5) *
pow(10, random.nextInt(51) - 25));
}
});
});
group('ulp', () {
double ulp(double value) {
final result = value.ulp;
if (value.isNaN) {
expect(result, isNaN);
} else {
expect(result, greaterThan(0));
expect((-value).ulp, result);
}
return result;
}

test('basic', () {
expect(ulp(1e-52), 1.8546030753437107e-68);
expect(ulp(1e-25), 1.1479437019748901e-41);
expect(ulp(1e-10), 1.2924697071141057e-26);
expect(ulp(0.25), 5.551115123125783e-17);
expect(ulp(0.5), 1.1102230246251565e-16);
expect(ulp(0.6), 1.1102230246251565e-16);
expect(ulp(1), 2.220446049250313e-16);
expect(ulp(1e10), 0.0000019073486328125);
expect(ulp(1e25), 2147483648.0);
expect(ulp(1e52), 1.329227995784916e+36);
});
test('special', () {
expect(ulp(0.0), double.minPositive);
expect(ulp(double.nan), isNaN);
expect(ulp(double.infinity), double.infinity);
expect(ulp(double.negativeInfinity), double.infinity);
});
});
group('nextTowards', () {
test('basic', () {
expect(-1.0.nextTowards(0.0), -0.9999999999999999);
expect(1.0.nextTowards(0.0), 0.9999999999999999);
});
test('special', () {
expect(double.nan.nextTowards(0.0), isNaN);
expect(double.infinity.nextTowards(0.0), double.maxFinite);
expect(double.negativeInfinity.nextTowards(0.0), -double.maxFinite);
expect(0.0.nextTowards(0.0), 0.0);
});
test('stress', () {
final random = Random(8913);
for (var i = 0; i < 1000; i++) {
final value = 2 *
(random.nextDouble() - 0.5) *
pow(10, random.nextInt(51) - 25);
final result = value.nextTowards(0.0);
if (value < 0.0) {
expect(result, value.nextUp);
} else if (value > 0.0) {
expect(result, value.nextDown);
} else {
expect(result, 0.0);
}
}
});
});
});
group('digits', () {
group('base 10', () {
test('int', () {
Expand Down

0 comments on commit c6d512a

Please sign in to comment.