Thread overview
Formatting -0.0 as 0.0
Oct 15, 2021
Bastiaan Veelo
Oct 15, 2021
Bastiaan Veelo
Oct 15, 2021
Elronnd
Oct 18, 2021
Bastiaan Veelo
October 15, 2021

std.format maintains the minus sign when formatting a negative zero floating point value (by design):

double r = -0.0;
writeln(r.format!"%#4.3f"); // -0.000

I am looking for a low-impact way to output negative zero as positive zero to prevent our users from raising their eyebrows.

There is a trick that would be somewhat acceptable if it worked consistently:

double r = -0.0;
writeln((r + 0).format!"%#4.3f"); // 0.000

However, we have found that in some places with some ldc options the +0 gets optimized out! I have not reduced the format used above where this happens, but here is another comparable example:

// ldc options: -ffast-math -enable-inlining
import std;

void main()
{
    double r = -0.0;
    writeln(__LINE__, ":\t", r.format!"% 4.3f"); // -0.000
    writeln(__LINE__, ":\t", (r + 0).format!"% 4.3f"); // 0.000
    writeln(__LINE__, ":\t", r.posZero.format!"% 4.3f"); // -0.000 SURPRISE!
}

T posZero(T)(T value) if (isFloatingPoint!T)
{
    return value + 0;
}

https://run.dlang.io/is/XmAhLM

The alternative to (r+0) that does work consistently is (r){return r == -0.0 ? 0.0 : r;}(r) but that's just too much noise, and so the best I can come up with is inserting a call to

T posZero(T)(T value) if (isFloatingPoint!T)
{
    return value == -0.0 ? 0.0 : value;
}

What I would like best is if there were a format specifier flag that does this conversion automatically, like "%> 4.3f". Does it make sense to add this? Is there a better way?

-- Bastiaan.

October 15, 2021

On 10/15/21 8:37 AM, Bastiaan Veelo wrote:

>

What I would like best is if there were a format specifier flag that does this conversion automatically, like "%> 4.3f". Does it make sense to add this? Is there a better way?

The only way I can think of that isn't really intrusive (like your posZero thing) is to define your own writef/format wrappers (what I'd do is check any doubles, and replace them when necessary).

Another thing to do is to catch it at the source. That is, when you store a value somewhere where it might be printed, if it's -0.0, change it to 0.0.

Note that if ldc is optimizing out an addition with 0, and that actually changes the observable results, that technically is an invalid optimization.

-Steve

October 15, 2021

Thank you Steve.

On Friday, 15 October 2021 at 14:09:32 UTC, Steven Schveighoffer wrote:

>

On 10/15/21 8:37 AM, Bastiaan Veelo wrote:

>

What I would like best is if there were a format specifier flag that does this conversion automatically, like "%> 4.3f". Does it make sense to add this? Is there a better way?

The only way I can think of that isn't really intrusive (like your posZero thing) is to define your own writef/format wrappers (what I'd do is check any doubles, and replace them when necessary).

The thing that is holding me back is that it feels backwards to wrap std to fix what I perceive as a usability issue, contra fixing std itself. Hence my proposal of extending the format specification -- although I find it complicated enough as it is. I am surprised that I am the first to have this problem.

I think I'm going to follow your suggestion nonetheless, as seeing posZero all over the place looks ridiculous...

>

Another thing to do is to catch it at the source. That is, when you store a value somewhere where it might be printed, if it's -0.0, change it to 0.0.

Is not an option in my situation, as it must work on machine translated code. For new code it would work, but I doubt our programmers will be conscious of the issue at all times. As we work with a left-handed coordinate system, multiplying Y with -1 happens frequently, and 0 is a common value for Y. Easy to miss a spot.

>

Note that if ldc is optimizing out an addition with 0, and that actually changes the observable results, that technically is an invalid optimization.

Issue filed: https://github.com/ldc-developers/ldc/issues/3851

Fun fact: I checked two other Pascal compilers: gpc prints -0.0 as 0.0 like our old compiler does, fpc is like D and prints it as -0.0.

--Bastiaan.

October 15, 2021

On 10/15/21 11:18 AM, Bastiaan Veelo wrote:

>

Thank you Steve.

On Friday, 15 October 2021 at 14:09:32 UTC, Steven Schveighoffer wrote:

>

On 10/15/21 8:37 AM, Bastiaan Veelo wrote:

>

What I would like best is if there were a format specifier flag that does this conversion automatically, like "%> 4.3f". Does it make sense to add this? Is there a better way?

The only way I can think of that isn't really intrusive (like your posZero thing) is to define your own writef/format wrappers (what I'd do is check any doubles, and replace them when necessary).

The thing that is holding me back is that it feels backwards to wrap std to fix what I perceive as a usability issue, contra fixing std itself. Hence my proposal of extending the format specification -- although I find it complicated enough as it is. I am surprised that I am the first to have this problem.

Well, it's based on C's implementation, so you are definitely not the first ;)

https://stackoverflow.com/questions/9657993/how-to-convert-negative-zero-to-positive-zero-in-c

That also has a good suggestion instead of using * 0, you can check if it's 0 and just assign 0 in that case.

-Steve

October 15, 2021
On Friday, 15 October 2021 at 14:09:32 UTC, Steven Schveighoffer wrote:
> Note that if ldc is optimizing out an addition with 0, and that actually changes the observable results, that technically is an invalid optimization.

I disagree.  With -ffast-math you're explicitly saying 'please feel free to make changes to expressions involving floating-point numbers which maintain all observable results only under the assumption that floating-point numbers have properties that they actually don't'.

Most usually, these changes are things like turning (x+y)+z into x+(y+z) because it schedules better, turning or x+y*z into an FMA.  Both of these can affect observable results because FP math is not associative, and because FMA is computed at high precision without an intermediate rounding stage.  Elimination of x+0.0 definitely falls under that umbrella.
October 16, 2021

On 10/15/21 6:26 PM, Elronnd wrote:

>

On Friday, 15 October 2021 at 14:09:32 UTC, Steven Schveighoffer wrote:

>

Note that if ldc is optimizing out an addition with 0, and that actually changes the observable results, that technically is an invalid optimization.

I disagree.  With -ffast-math you're explicitly saying 'please feel free to make changes to expressions involving floating-point numbers which maintain all observable results only under the assumption that floating-point numbers have properties that they actually don't'.

Yeah you are probably right. I didn't realize that was a special optimization flag.

-Steve

October 18, 2021

On Friday, 15 October 2021 at 14:09:32 UTC, Steven Schveighoffer wrote:

>

The only way I can think of that isn't really intrusive (like your posZero thing) is to define your own writef/format wrappers (what I'd do is check any doubles, and replace them when necessary).

I have settled on a wrapper. @kinke has helped me to deal with fast-math, so this can be branchless.

import std.traits : isSomeChar, isSomeString;

/** Wraps std.format.format converting any negative zero floating point to positive. */
immutable(Char)[] fmt(Char, Args...)(in Char[] spec, Args args)
if (isSomeChar!Char)
{
    import std.format : format;

    return format(spec, PosZero!args);
}

/** idem */
typeof(spec) fmt(alias spec, Args...)(Args args)
if (isSomeString!(typeof(spec)))
{
    import std.format : format;

    return format!spec(PosZero!args);
}

// Maps args, adding 0.0 if floating point.
private template PosZero(args...)
{
    import std.meta : staticMap;

    template posZero(alias arg)
    {
        import std.traits : isFloatingPoint;

        static if (isFloatingPoint!(typeof(arg)))
        {
            version (LDC)
            {
                import ldc.attributes : llvmFastMathFlag;
                @llvmFastMathFlag("clear") auto posZero() {return arg + 0.0;}
            }
            else
                auto posZero() {return arg + 0.0;}
        }
        else
            alias posZero = arg;
    }

    alias PosZero = staticMap!(posZero, args);
}

unittest
{
    import std.format : format;

    //assert((-0.0).format!"%3.1f" == "-0.0");
    assert((-0.0).fmt!"%3.1f" == "0.0");
    //assert(format("%3.1f", -0.0) == "-0.0");
    assert(fmt("%3.1f", -0.0) == "0.0");

    string foo(immutable double d)
    {
        return d.fmt!"%3.1f";
    }
    assert(foo(-0.0) == "0.0");

    //assert(format!"%d;%3.1f;%3.1f;%3.1f;%s"(42, -0.0, 3.14, -0.0, "!") == "42;-0.0;3.1;-0.0;!");
    assert(fmt!"%d;%3.1f;%3.1f;%3.1f;%s"(42, -0.0, 3.14, -0.0f, "!") == "42;0.0;3.1;0.0;!");
    //assert(format("%d;%3.1f;%3.1f;%3.1f;%s", 42, -0.0, 3.14, -0.0, "!") == "42;-0.0;3.1;-0.0;!");
    assert(fmt("%d;%3.1f;%3.1f;%3.1f;%s", 42, -0.0, 3.14, -0.0f, "!") == "42;0.0;3.1;0.0;!");
    assert(fmt!""() == format!""());
    assert(fmt("") == format(""));
}

Love this language!

-- Bastiaan.