Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make std.math.fmax() and .fmin() variadic. Add explicit NaN handling. #4346

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions std/algorithm/comparison.d
Original file line number Diff line number Diff line change
Expand Up @@ -1261,7 +1261,9 @@ levenshteinDistanceAndPath(alias equals = (a,b) => a == b, Range1, Range2)

// max
/**
Iterates the passed arguments and return the maximum value.
Iterates the passed arguments and return the maximum value. Note that floating-point
NaN values are not handled in a consistent way by this function; see `std.math.fmax`
if this is a problem.

Params:
args = The values to select the maximum from. At least two arguments must
Expand All @@ -1273,6 +1275,7 @@ Returns:

See_Also:
$(XREF_PACK algorithm,searching,maxElement)
$(XREF_PACK math,fmax)
*/
MaxType!T max(T...)(T args)
if (T.length >= 2)
Expand Down Expand Up @@ -1380,13 +1383,16 @@ private template MinType(T...)

// min
/**
Iterates the passed arguments and returns the minimum value.
Iterates the passed arguments and returns the minimum value. Note that floating-point
NaN values are not handled in a consistent way by this function; see `std.math.fmin`
if this is a problem.

Params: args = The values to select the minimum from. At least two arguments
must be passed, and they must be comparable with `<`.
Returns: The minimum of the passed-in values.
See_Also:
$(XREF_PACK algorithm,searching,minElement)
$(XREF_PACK math,fmin)
*/
MinType!T min(T...)(T args)
if (T.length >= 2)
Expand Down
283 changes: 279 additions & 4 deletions std/math.d
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ import core.bitop;
import core.math;
import core.stdc.math;
import std.traits;
import std.meta;

version(LDC)
{
Expand Down Expand Up @@ -5884,14 +5885,288 @@ T nextafter(T)(const T x, const T y) @safe pure nothrow @nogc
real fdim(real x, real y) @safe pure nothrow @nogc { return (x > y) ? x - y : +0.0; }

/****************************************
* Returns the larger of x and y.
* Returns the largest (most positive) element of `nums`.
*
* Elements are compared after implicit conversion to a common floating-point
* type. If `nums` is empty or any element `isNaN`, a `nan` value will be returned.
*/
real fmax(real x, real y) @safe pure nothrow @nogc { return x > y ? x : y; }
auto fmax(T ...)(T nums)
if (allSatisfy!(ApplyRight!(isImplicitlyConvertible, real), T))
{
pragma(inline, true);

alias F = AutoFImumType!T;
static if (T.length <= 4)
return fimumImpl!(">", T.length, F)(nums);
else
return fimumImpl!(">", F)(nums);
}

/// ditto
auto fmax(F, T ...)(T nums)
if (isFloatingPoint!F && allSatisfy!(ApplyRight!(isImplicitlyConvertible, F), T))
{
pragma(inline, true);

alias R = OriginalType!(Unqual!F);
static if (T.length <= 4)
return fimumImpl!(">", T.length, R)(nums);
else
return fimumImpl!(">", R)(nums);
}

/// ditto
auto fmax(F)(const F[] nums) pure @safe nothrow @nogc
if (isFloatingPoint!F)
{
pragma(inline, true);

alias R = OriginalType!(Unqual!F);
return fimumImpl!(">", R)(nums);
}

/// ditto
auto fmax(F, size_t count)(auto ref const(F[count]) nums) pure @safe nothrow @nogc
if (isFloatingPoint!F)
{
pragma(inline, true);

alias R = OriginalType!(Unqual!F);
static if (count <= 4)
return fimumImpl!(">", count, R)(nums);
else
return fimumImpl!(">", R)(nums[]);
}

///
pure @safe nothrow @nogc unittest
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: maybe you can add an additional, simpler unittest case that can be shown to the user at the documentation. This one is quite technical

// Any number of arguments is supported:
assert(fmax(101.0, -5.0, 7.3723e121, double.epsilon) == 7.3723e121);
assert(fmax(1) == 1.0);
assert(fmax(ubyte.max, 25.0f) == 255.0f);
// Static and dynamic floating-point arrays, too:
double[7] nums = [0.0, double.min_normal, 0.5, -1.0, -double.max, double.infinity, 8];
assert(fmax(nums) == double.infinity);
assert(fmax(nums[1 .. 3]) == 0.5);

// NaN handling:
assert(fmax().isNaN);
assert(fmax(13, -431_315, long.max, real.nan, float.infinity).isNaN);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we already agree that infinity should return NaN?

as @klickverbot pointed us to this great discussion

min that propagates NaN except when one of the arguments is -∞, in order to preserve the identity min(-∞,y)==-∞.

(same for max)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the purposes of NaN values is to warn when a complex number is generated. This would be bad:

assert(fmax(sqrt(-1.0), double.infinity) is double.infinity);

If we're going to have NaN poisoning, then we should have NaN poisoning (at least by default). Almost-but-not-quite poisoning will likely lead to more bug reports down the line.

If there is high demand for the above behaviour though, I suppose I could add another template parameter.


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to be sure I would add additional checks, e.g.

  • `float.min
  • -float.infinity
  • float.infinity, double.infinity
  • double.inifinity, real.infinity

// The return type is always floating-point, and mixed signed-unsigned comparisons
// are safe, even if all the arguments are integers.
assert(fmax(int.min, cast(uint)int.max) is cast(double)int.max);
// The F template parameter can be used to request a specific floating-point type:
static assert(is(typeof(fmax!float(1u)) == float));
static assert(is(typeof(fmax!double(1L)) == double));
static assert(is(typeof(fmax!real(1.0f)) == real));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add a check with std.complex and std.bigint too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std.complex

The concept of a maximum or minimum value is not well-defined for complex numbers. (That's why std.complex.Complex does not implement opCmp().)

std.bigint

That's outside the scope of fmax() and fmin(); as I state in the documentation, only types that can be implicitly converted to a floating-point type are supported.


/****************************************
* Returns the smaller of x and y.
* Returns the smallest (most negative) element of `nums`.
*
* Elements are compared after implicit conversion to a common floating-point
* type. If `nums` is empty or any element `isNaN`, a `nan` value will be returned.
*/
real fmin(real x, real y) @safe pure nothrow @nogc { return x < y ? x : y; }
auto fmin(T ...)(T nums)
if (allSatisfy!(ApplyRight!(isImplicitlyConvertible, real), T))
{
pragma(inline, true);

alias F = AutoFImumType!T;
static if (T.length <= 4)
return fimumImpl!("<", T.length, F)(nums);
else
return fimumImpl!("<", F)(nums);
}

/// ditto
auto fmin(F, T ...)(T nums)
if (isFloatingPoint!F && allSatisfy!(ApplyRight!(isImplicitlyConvertible, F), T))
{
pragma(inline, true);

alias R = OriginalType!(Unqual!F);
static if (T.length <= 4)
return fimumImpl!("<", T.length, R)(nums);
else
return fimumImpl!("<", R)(nums);
}

/// ditto
auto fmin(F)(const F[] nums) pure @safe nothrow @nogc
if (isFloatingPoint!F)
{
pragma(inline, true);

alias R = OriginalType!(Unqual!F);
return fimumImpl!("<", R)(nums);
}

/// ditto
auto fmin(F, size_t count)(auto ref const(F[count]) nums) pure @safe nothrow @nogc
if (isFloatingPoint!F)
{
pragma(inline, true);

alias R = OriginalType!(Unqual!F);
static if (count <= 4)
return fimumImpl!("<", count, R)(nums);
else
return fimumImpl!("<", R)(nums[]);
}

///
pure @safe nothrow @nogc unittest
{
// Any number of arguments is supported:
assert(fmin(101.0, -5.0, 7.3723e121, double.epsilon) == -5.0);
assert(fmin(1) == 1.0);
assert(fmin(ubyte.max, 25.0f) == 25.0f);
// Static and dynamic floating-point arrays, too:
double[7] nums = [0.0, double.min_normal, 0.5, -1.0, -double.max, double.infinity, 8];
assert(fmin(nums) == -double.max);
assert(fmin(nums[1 .. 3]) == double.min_normal);

// NaN handling:
assert(fmin().isNaN);
assert(fmin(13, -431_315, long.max, real.nan, float.infinity).isNaN);

// The return type is always floating-point, and mixed signed-unsigned comparisons
// are safe, even if all the arguments are integers.
assert(fmin(-1, cast(uint)int.max) is -1.0);
// The F template parameter can be used to request a specific floating-point type:
static assert(is(typeof(fmin!float(1u)) == float));
static assert(is(typeof(fmin!double(1L)) == double));
static assert(is(typeof(fmin!real(1.0f)) == real));
}

private template AutoFImumType(T ...)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's wrong with CommonType?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static assert(is(CommonType!(long, float) == float));
static assert(is(AutoFImumType!(long, float) == real));

D allows implicit conversions from integral types to floating-point types with much less precision. Thus, I did not use CommonType both because:

  • I want fmax() and fmin() to be reasonably precise (using at least double for int and long), and
  • The old signature up-casts everything to real, which is fully precise even with ulong on x86(_64), meaning that switching to CommonType would be a breaking change.

private enum int reqPrec =
{
int max = (T.length == 0) ? double.mant_dig : 0;
foreach (V; T)
{
int prec;
static if (is(V : short) || is(V : ushort))
prec = 8 * ushort.sizeof;
else static if (is(V : int) || is(V : uint))
prec = 8 * uint.sizeof;
else static if (is(V : long) || is(V : ulong))
prec = 8 * ulong.sizeof;
else static if (is(typeof(typeof(V.init + 1).mant_dig)))
prec = typeof(V.init + 1).mant_dig;
else
prec = real.mant_dig;

if (prec > max)
max = prec;
}
return max;
}();

static if (reqPrec <= float.mant_dig)
alias AutoFImumType = float;
else static if (reqPrec <= double.mant_dig)
alias AutoFImumType = double;
else
alias AutoFImumType = real;
}

unittest
{
foreach (F; AliasSeq!(float, double, real))
static assert(is(AutoFImumType!F == F));
foreach (I; AliasSeq!(bool, byte, ubyte, char, short, ushort, wchar))
static assert(is(AutoFImumType!I == float));
foreach (I; AliasSeq!(int, uint, dchar))
static assert(is(AutoFImumType!I == double));
foreach (I; AliasSeq!(long, ulong))
static assert(is(AutoFImumType!I == real));

enum Foo : float { foo = 1.0f }
static assert(is(AutoFImumType!Foo == float));
enum Bar : long { bar = 1 }
static assert(is(AutoFImumType!Bar == real));

struct Baz
{
short baz;
alias baz this;
}
static assert(is(AutoFImumType!Baz == float));

static assert(is(AutoFImumType!(ushort, Foo) == float));
static assert(is(AutoFImumType!(uint, wchar) == double));
static assert(is(AutoFImumType!(Baz, uint, float) == double));
static assert(is(AutoFImumType!(byte, Bar, long) == real));
static assert(is(AutoFImumType!(real, int) == real));
static assert(is(AutoFImumType!(float, real) == real));
static assert(is(AutoFImumType!(double, real) == real));
static assert(is(AutoFImumType!(real, double, float) == real));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't check float, real or double, real nor float, double, real

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are an infinite number of possible combinations, because T can be any length. What's special about those combinations that needs to be tested?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You currently don't have any check with a combination of two floating types

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


private F fimumImpl(string op, size_t count, F)(F[count] nums ...) pure @safe nothrow @nogc
if (isFloatingPoint!F && count <= 4)
{
static if (count <= 2)
pragma(inline, true);
F ret = F.nan;

static if (count > 1)
{
enum ax = 0,
bx = 1 + (count > 2);

static if (count > 2)
{
static if (count > 3)
nums[bx] = fimumImpl!(op, 2)(nums[2], nums[3]);
else
nums[bx] = nums[2];

nums[ax] = fimumImpl!(op, 2)(nums[0], nums[1]);
}

ret = (mixin(`nums[ax] ` ~ op ~ `= nums[bx]`) || nums[ax].isNaN) ?
nums[ax] : nums[bx];
}
else static if (count > 0)
ret = nums[0];

return ret;
}

private F fimumImpl(string op, F)(const(F)[] nums ...) pure @safe nothrow @nogc
if (isFloatingPoint!F)
{
enum anti = (mixin(`1 ` ~ op ~ ` -1`) ? -1 : 1) * F.infinity;
Unqual!F ret = anti;

while (nums.length >= 4)
{
F[4] num4 = nums[0 .. 4];
const m4 = fimumImpl!(op, 4)(num4);
nums = nums[4 .. $];

if (mixin(`m4 ` ~ op ~ ` ret`))
ret = m4;
else if (m4.isNaN)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be the first comparison?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No.

return m4;
}

foreach (num1; nums)
{
if (mixin(`num1 ` ~ op ~ ` ret`))
ret = num1;
else if (num1.isNaN)
return num1;
}

return ret;
}

/**************************************
* Returns (x * y) + z, rounding only once according to the
Expand Down