'Unsolvable' Issues with .doubleToStringDec()

Discussion forum for C++ and script developers who are using the QCAD development platform or who are looking to contribute to QCAD (translations, documentation, etc).

Moderator: andrew

Forum rules

Always indicate your operating system and QCAD version.

Attach drawing files, scripts and screenshots.

Post one question per topic.

Post Reply
CVH
Premier Member
Posts: 3415
Joined: Wed Sep 27, 2017 4:17 pm

'Unsolvable' Issues with .doubleToStringDec()

Post by CVH » Wed Aug 19, 2020 4:46 pm

Andrew,

In need of a fast and reliable textual number formatting function I tried many ways.
From own hand and sourced online or mixed. I made up some candidates to evaluate.

Number.toFixed(accuracy) or parseFloat(value).toFixed(accuracy)
Both fail at some points. A) doesn't round ..5 always up, B) solves this only for some.
numberToString() use sprintf.js and needs post formatting too.
It returns a comma in my case and a leading zero.

- FAST! It is called tirelessly to trial number formatting for 'Tile2Hatch'.
- formatting a parseFloat() as short as possible.
- rounding tie breaking half away from zero.
- without leading- and trailing zeros. (optional with any)
- with a dot as decimal separator. (optional with custom)

For this I also evaluated RUnit.doubleToStringDec()
A promising build-in function and compiled still very fast although the use of sprintf (not .js I presume).
At first glance only the default for the use of a leading zero differs.

It took me a while to figure out that the custom decimal point should be a character code, a number.
Looking into: https://qcad.org/doc/qcad/3.0/developer ... 124e3f2c3f
RDEFAULT_DOT is defined as a '.' (dot) not as value 46.
What did the trick was:

Code: Select all

REcmaUnit.cpp ...
      // argument isStandardType
      char
      a4 =
      (char)
                    
       context->argument( 4 ).
       toNumber();
Aha, char & Number ... Character number and that worked out fine.

But ...
In the end, I can't get the 'NO leadingZero' to work. Using a LZ is perstistent.

Code: Select all

var testcase = 0.0125;
var accuracy = 6;
var decPnt = "p";
   var reply0 = RUnit.doubleToStringDec(testcase, accuracy);                // 0.0125 defaults LZ=true TZ=false
   var reply1 = RUnit.doubleToStringDec(testcase, accuracy, true);          // 0.0125 default TZ=false
   var reply2 = RUnit.doubleToStringDec(testcase, accuracy, false);         // 0.0125 default TZ=false FAILS LZ=false
   var reply3 = RUnit.doubleToStringDec(testcase, accuracy, true, true);    // 0.012500
   var reply4 = RUnit.doubleToStringDec(testcase, accuracy, true, false);   // 0.0125
   var reply5 = RUnit.doubleToStringDec(testcase, accuracy, false, true);   // 0.012500 FAILS LZ=false
   var reply6 = RUnit.doubleToStringDec(testcase, accuracy, false, false);  // 0.0125 FAILS LZ=false
   var reply7 = RUnit.doubleToStringDec(testcase, accuracy, true, true, decPnt);  // 0p012500 
   var reply8 = RUnit.doubleToStringDec(testcase, accuracy, true, false, decPnt);  // 0p0125
// Next all FAIL LZ=false
   var reply9 = RUnit.doubleToStringDec(testcase, accuracy, false, true, decPnt);
   var replyA = RUnit.doubleToStringDec(testcase, accuracy, false, false, decPnt);
   var replyB = RUnit.doubleToStringDec(testcase, accuracy, false, false, ",".charCodeAt(0));
   var replyC = RUnit.doubleToStringDec(testcase, accuracy, false, false, 44);
As script reference, there is only 1 usage in scripts\Draw\Dimension\Dimension.js with 2 arguments.

Beyond scripts it get fuzzy:

Code: Select all

RUnit.h ...
    static QString doubleToString(double value, double prec,
        bool showLeadingZeroes=true, bool showTrailingZeroes=false,
        char decimalSeparator=RDEFAULT_DOT);
    static QString doubleToString(double value, int prec,
        bool showLeadingZeroes=true, bool showTrailingZeroes=false,
        char decimalSeparator=RDEFAULT_DOT);

    // workaround: make the second version also accessible by scripts:
    static QString doubleToStringDec(double value, int prec,
        bool showLeadingZeroes=true, bool showTrailingZeroes=false,
        char decimalSeparator=RDEFAULT_DOT) {

        return doubleToString(value, prec, showLeadingZeroes, showTrailingZeroes, decimalSeparator);
    }
So, doubleToStringDec is a workaround calling doubleToString 2/2 with 'int prec'.
Also see: https://qcad.org/doc/qcad/3.0/developer ... e195df67c6
The two headers look exactly the same apart from the nature of 'prec'.

This isn't true looking at RUnit.cpp:

Code: Select all

QString RUnit::doubleToString(double value, double prec,
        bool showLeadingZeroes, bool showTrailingZeroes,
        char decimalSeparator) { ... }
QString RUnit::doubleToString(double value, int prec,
        bool /*showLeadingZeroes*/, bool showTrailingZeroes,
        char decimalSeparator) { ... }
I am afraid that /*showLeadingZeroes*/ blocks the use of NO LeadingZeroes.
And if I can follow it correctly then doubleToString 1/2 calls doubleToStringDec what is calling doubleToString 2/2 ...

Further:
Do I see limitations in what can be handled:

Code: Select all

doubleToString 1/2 ...
    if (prec>1.0e-12) {
    exaStr = doubleToStringDec(prec, 10); 
doubleToString 2/2 ...
    adds +/- fuzz = 1.0e-13 to sprintf
Here we are talking of decimal digits.
A double with 15.95 significant digits can look like 0.00000123451234512345(6).
With 10,12 or 13 decimal digits it looks like 0.0000012345(12(3)) and has at most 5-8 significant digits.

Troubled.
Can you explain all this more in detail?

Regards,
CVH
Last edited by CVH on Mon Aug 24, 2020 10:36 am, edited 1 time in total.

User avatar
andrew
Site Admin
Posts: 9037
Joined: Fri Mar 30, 2007 6:07 am

Re: Issues with .doubleToStringDec()

Post by andrew » Wed Aug 19, 2020 5:50 pm

I can confirm that the parameter showLeadingZeroes is ignored in all these functions. It's intended for possible future use as there is a DXF/DWG variable that controls this for dimension labels. However, this is not implemented for QCAD and it's also not on my horizon at the moment.

User avatar
andrew
Site Admin
Posts: 9037
Joined: Fri Mar 30, 2007 6:07 am

Re: Issues with .doubleToStringDec()

Post by andrew » Thu Aug 20, 2020 8:06 am

You might want to look into numeral.js (http://numeraljs.com). This could be included and used in the QCAD script environment. It's already used by scripts/Misc/MiscDraw/Counter.

CVH
Premier Member
Posts: 3415
Joined: Wed Sep 27, 2017 4:17 pm

Re: Issues with .doubleToStringDec()

Post by CVH » Thu Aug 20, 2020 9:59 am

Back to the drawing board.

Found this: I am also surprised that it has been tolerated for 50 years.
https://dfkaye.wordpress.com/2017/12/06 ... t-fixable/

The second update of the 'ADDENDUM 28 MAY 2020' is of no use:
toLocaleString failed 'options' and a test for the implementation of 'options' proposed here fails too:
https://developer.mozilla.org/en-US/doc ... caleString

The firts update is similar as padding in a literal '1' to avoid rounding errors by toFixed the last 50 years.
(There is an exponentional fix too)
Implemented:

Code: Select all

Tile2Hatch.formatNumVal = function(value, accuracy, leadingZero, trailingZeros, decPnt) {
    var valueS, split, valueP1;

    switch (arguments.length) {
    case 0:
    case 1:
        return undefined;         // Dismiss wrong usage
    case 2:
        leadingZero = false;      // Default without leadingZero
    case 3:
        trailingZeros = false;    // Default without trailingZeros
    case 4:
        decPnt = ".";             // Default decimal point
    default:
    // -> Continue with all (or more) arguments

//debugger;
        split = value.toString().split('.');
        valueP1 = Number(!split[1]             // Whithout a fractional part
          ? split[0]                           // Use Integer part
          : (split[1].length >= accuracy       // Else: When the fractional part isNOT shorter as accuracy
            ? split.join('.') + '1'            // Rejoin parts and add '1'
            : split.join('.')));               // Else: Rejoin parts
        valueS = valueP1.toFixed(accuracy);
        // Optionally clear trailing zeros and the decimal point if appropriate:
        if (!trailingZeros) {
            valueS = valueS.replace(/(?:(\.\d*?[1-9]+)|\.)0*$/,"\$1");
        }
         // Optionally clear leading zero:
        if (!leadingZero) {
            valueS = valueS.replace(/^(-?)0+\./, "\$1\.");
        }
        // With a rounded and optionally trimmed value equal to zero simply return '0':
        // May need fuzzy compare
        if (parseFloat(valueS) === 0.0) {
            return "0";
        }
        // Return the value as text with the custom decimal point:
        return valueS.replace(".",decPnt);
    }
}       
It is correct so far and fast for a script but slow because it's just a script.

RUnit.doubleToStringDec() followed by optionally clearing the leading zero is still faster.

Numerals is fairly similar as the way Excel handles custom formats.
1013 lines of code ... multitude of options ... I wonder if it is fast.
One downer already: constructing formats need a string.repeat what is not implemented.
eg. from your hand:

Code: Select all

function str_repeat(input, multiplier) {
    return new Array(multiplier + 1).join(input)
}
Another looping function that will make overtime constructing formatStrings from 'X decimal digits' over and over again.

I think its time I learn to compile my scripts ....

Regards,
CVH

CVH
Premier Member
Posts: 3415
Joined: Wed Sep 27, 2017 4:17 pm

Re: Issues with .doubleToStringDec()

Post by CVH » Thu Aug 20, 2020 12:43 pm

Reading up on the topic I came across some disturbing statements ....
I have to vent this ...

Highly scholarly persons agree in that 0.049999999999999923 obviously doesn't round to 0.1 ????
(because the second decimal digit is a 4)
:oops:
It was a good thing that I sat down while reading it.

Both of my children know from the age of 6 that rounding start at the end of a number.
And certainly NOT somewhere in the middle.
3 rounds down to 0.
20 rounds down to 00.
900 rounds up to 1000 and carries 1 over.
That continues to 49(+1)0.
What will result in 0.05 and the 5 rounds up, carries 1 over, to give 0.1 what is correct.
(With tie-breaking away from zero)

The absolute fault converting from binary double to decimal is incredibly small.
In the order of 1/(2^52) of the most significant decimal digit.
That flaw is at the end!
It makes no sense correcting this somewhere upfront.

So the flaw is that rounding and all the related functions look at the next decimal digit.
Forgetting to look at what is trailing that ...

A problem 50 years old, a kid of 6 can fix in a blink.


My 2 cents
CVH

CVH
Premier Member
Posts: 3415
Joined: Wed Sep 27, 2017 4:17 pm

Re: Issues with .doubleToStringDec()

Post by CVH » Fri Aug 21, 2020 9:00 am

Update:
.doubleToStringDec() fails beyond 12 decimal digits.
numeral.js is rather slower and the formatString needs pre-formatting.

So I tweaked the hell out my version.
Only 15 lines of code in total. ( the header is twice that :wink: )
All native script functions.

Like what I see:
T2Hnumber formatting.png
T2Hnumber formatting.png (12.79 KiB) Viewed 4326 times

Code: Select all

/**
 * Format a numerical value for textual output.
 * With or without leading and/or trailing zero(s) and with a configurable decimal point.
 * Newer art requires numerous calls to a fast specialized function to fit all the data in a single definition line.
 * A replacement for the overkill with numberToString() what uses the sprintf script while also needing post formatting.
 * \author CVH
 *
 * The RUnit Class has similar functions:
 * RUnit.doubleToString(value, prec, showLeadingZeroes = true, showTrailingZeroes = false, decimalSeparator = RDEFAULT_DOT)
 * RUnit.doubleToString(value, acc, showLeadingZeroes = true, showTrailingZeroes = false, decimalSeparator = RDEFAULT_DOT)
 * RUnit.doubleToStringDec(value, acc, showLeadingZeroes = true, showTrailingZeroes = false, decimalSeparator = RDEFAULT_DOT)
 * # Known Issue # showLeadingZeroes is not an option. See: https://qcad.org/rsforum/posting.php?mode=reply&f=30&t=7512#pr29188
 * # Known Issue # NOT functional beyond 12 decimal digits.
 *
 * Under JavaScript a string representation from a value has:
 * - no trailing zeros
 * - a leading zero when the integer part is zero
 * - a dot as decimal separator
 *
 * # Known Issue # Weak, doesn't include checks for proper useage in favor for speed.
 * # Issue Fixed # Apart from the remarks online, the rounding by .toFixed(n) off full digits SEEMS ok.
 * # Done # Not all x.xxx5 rounds up. Figure it out!!! Fixed.
 * # Done # Prone to speed optimization.
 *
 * \param value                  The value to be formatted.    (Double)
 * \param accuracy               The number of decimal digits.    (+oInteger 0-20?)
 * \param (leadingZero)          Force leading zero.  Default=false    (Boolean)
 * \param (trailingZeros)        Force trailing zeros.  Default=false    (Boolean)
 * \param (decPnt)               The preffered decimal point.  Default=dot    (String)
 *
 * \return The value as a formatted string. (or undefined)    (String)
 */
Tile2Hatch.formatNumVal = function(value, accuracy, leadingZero, trailingZeros, decPnt) {
    var valueS, split; 
    leadingZero || (leadingZero = false);
    trailingZeros || (trailingZeros = false);
    decPnt || (decPnt = ".");
    split = value.toString().split(".");
    valueS = Number(!split[1]
      ? split[0]
      : (split[1].length >= accuracy
        ? split.join(".") + "1"
        : split.join("."))).toFixed(accuracy);
    trailingZeros || (valueS = valueS.replace(/(?:(\.\d*?[1-9]+)|\.)0*$/, "\$1"));
    leadingZero || (valueS = valueS.replace(/^(-?)0\./, "\$1\."));
    return (valueS === "-0") ? "0" : valueS.replace(".",decPnt);
};
Last edited by CVH on Fri Oct 30, 2020 8:20 am, edited 1 time in total.

CVH
Premier Member
Posts: 3415
Joined: Wed Sep 27, 2017 4:17 pm

Re: Issues with .doubleToStringDec()

Post by CVH » Mon Aug 24, 2020 10:35 am

Andrew, All,

Changed the topic to 'Unsolvable'.
Meaning: It can't be solved complying to the requirements. :(

From all evaluated solutions the last updated version is the fastest and is stable over a wider range.
A range that would exceed the limits that any CAD data is in.
So, if someone ever needs fast and proper text from number formatting ...
1 flaw: +/-zero never formats ... it is always returned as a '0'. :oops:

Padding in a '1' , the exponential versions and many more of such routines ...
all rely on floating point error accumulation in favor of rounding decimal numbers ending in about, but possible just less than, 1/2 up.
Every calculation, type transition, even language internal variable type handling affects the real world certainty off a floating point value.

The solution is >>> 1 ulp <<<. e_ugeek

In short and not explicable correct as such: e_geek
The half value of the uncertainty of the LSB of the binary number with 52 bits and an exponent of +/-10 bits expressed in decimal.
That is if we take the value to format 'as is' and disregard what calculations it resulted from ... :roll:

Calculating ulp's is again time consuming.
What can be avoided for 9 out of 10.
It's only the decimal tie breaking that matters.

Will come back on this. for me this topic is closed. :wink:
Regards,
CVH

Post Reply

Return to “QCAD Programming, Script Programming and Contributing”