www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Formatting -0.0 as 0.0

reply Bastiaan Veelo <Bastiaan Veelo.net> writes:
`std.format` maintains the minus sign when formatting a negative 
zero floating point value ([by 
design](https://forum.dlang.org/post/drkiaf$259l$1 digitaldaemon.com)):
```d
double r = -0.0;

```

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:
```d
double r = -0.0;

```
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:
```d
// 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
```d
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.
Oct 15 2021
parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
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
Oct 15 2021
next sibling parent reply Bastiaan Veelo <Bastiaan Veelo.net> writes:
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.
Oct 15 2021
parent Steven Schveighoffer <schveiguy gmail.com> writes:
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
Oct 15 2021
prev sibling next sibling parent reply Elronnd <elronnd elronnd.net> writes:
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.
Oct 15 2021
parent Steven Schveighoffer <schveiguy gmail.com> writes:
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
Oct 16 2021
prev sibling parent Bastiaan Veelo <Bastiaan Veelo.net> writes:
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. ```d 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.
Oct 18 2021