Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Formatting timers without losing accuracy? [duplicate]

I have an array of start/stop times. I basically want to display the time it took for each entry, as well as the total time for all of them. Here is the code I wrote to try to do that:

function timeFormatter (milliseconds) {
  const padZero = (time) => `0${time}`.slice(-2);

  const minutes = padZero(milliseconds / 60000 | 0);
  const seconds = padZero((milliseconds / 1000 | 0) % 60);
  const centiseconds = padZero((milliseconds / 10 | 0) % 100);

  return `${minutes} : ${seconds} . ${centiseconds}`;
}

// Example stopwatch times
const timeIntervals = [
  { startTime: 1470679294008, stopTime: 1470679300609 },
  { startTime: 1470679306278, stopTime: 1470679314647 },
  { startTime: 1470679319718, stopTime: 1470679326693 },
  { startTime: 1470679331229, stopTime: 1470679336420 }
];

// Calculate time it took for each entry
const times = timeIntervals.map(time => time.stopTime - time.startTime);

// Run the timeFormatter on each individual time
const individualTimes = times.map(timeFormatter);

// Run the timeFormatter on the sum of all the times
const mainTimer = timeFormatter(times.reduce((a, b) => a + b));

/**
     * [
     *   '00 : 06 . 60',
     *   '00 : 08 . 36',
     *   '00 : 06 . 97',
     *   '00 : 05 . 19'
     * ]
     */
console.log(individualTimes);

/**
     * 00 : 27 . 13
     */
console.log(mainTimer);

However, I am losing accuracy. As you can see, the individual times don't add up to the mainTimer value. It is always off by .01 - .03 no matter what the times are.

Is there a way I can make sure that the times only display two places, but still add up correctly? Any help would be appreciated.

I also have this on a JSFiddle where it's easier to run.


EDIT: The current answer did work for the case I provided above, but it does not work for all cases like this one.

like image 721
Saad Avatar asked Aug 14 '16 05:08

Saad


3 Answers

Problem

You are losing accuracy with each display of the rounded time. The more laps you have, the bigger the issue can be:

╔══════╦════════════════════════════════╗
║ Lap  ║         Total time (ms)        ║
║ Time ╠═══════╦═════════════╦══════════╣
║ (ms) ║    JS ║  Real World ║  Display ║
╠══════╬═══════╬═════════════╬══════════╣
║ 3157 ║  3157 ║  3157.5±0.5 ║  3160±5  ║
║ 2639 ║  5796 ║  5797.0±1   ║  5800±10 ║
║ 3287 ║  9083 ║  9084.5±1.5 ║  9090±15 ║
║ 3106 ║ 12189 ║ 12191.0±2   ║ 12200±20 ║
╚══════╩═══════╩═════════════╩══════════╝

The different totals actually overlap each other, once you take the tolerance into account:

  • JS time = 12189
  • Actual time = 12189 to 12193
  • Displayed time = 12180 to 12240

In other words, by adding up the displayed time, the lost in displayed accuracy is also added. Failing to account for this lost by the human is the real problem.

Solutions

  1. Increase the displayed precision (if you don't round numbers, you won't lost anything)
  2. Use the displayed sum which is less accurate, but not incorrect if you also state the tolerance (±).
  3. Detect the issue and display a message.

Here is a demo of solution 3.

function run ( set ) {
   show ( set === 0
      ? // Good set
        [ { startTime: 1470679294008, stopTime: 1470679300609 },
          { startTime: 1470679306278, stopTime: 1470679314647 },
          { startTime: 1470679319718, stopTime: 1470679326693 },
          { startTime: 1470679331229, stopTime: 1470679336420 } ]
      : // Bad set
        [ { startTime: 1472104779284,  stopTime: 1472104782441 },
          { startTime: 1472104782442,  stopTime: 1472104785081 },
          { startTime: 1472104785081,  stopTime: 1472104788368 },
          { startTime: 1472104788369,  stopTime: 1472104791475 }, ] );
}

function show ( timeIntervals ) {
   const sum = (a, b) => a + b;

   const roundTime = (ms) => Math.round(ms/10);

   function timeFormatter (centi) {
     const padZero = (time) => `0${~~time}`.slice(-2);

     const minutes = padZero(centi / 6000);
     const seconds = padZero((centi / 100) % 60);
     const centiseconds = padZero(centi % 100);

     return `${minutes} : ${seconds} . ${centiseconds} `;
   }

   // Calculate time it took for each entry.
   const times = timeIntervals.map(time => time.stopTime - time.startTime);

   // Rou and run the timeFormatter on each individual time
   const roundedTimes = times.map(roundTime);
   const individualTimes = roundedTimes.map(timeFormatter);
   // Calculate sum of displayed time
   const displayedSum = roundedTimes.reduce(sum);

   // Sum time and run timeFormatter
   const totalTime = roundTime( times.reduce(sum) );
   const mainTimer = timeFormatter(totalTime);

   let html = '<ol><li>' + individualTimes.join('<li>') + '</ol>Sum: ' + mainTimer;
   // Show warning if sum of rounded time is different.
   if ( displayedSum !== totalTime )
      html += ' (Rounding error corrected)';

   document.querySelector('div').innerHTML = html;
}

run(1);
<button onclick='run(0)'>Perfect</button>
<button onclick='run(1)'>Opps</button>
<div></div>

Or just live with it

All timers, even physical ones, must live with this rounding problem. Have you seen any timer that make such a disclaimer?

For a timer, showing a more accurate total time is certainly more correct even if it is inconsistent.

If you paid attention, you should see / realise that javascript time also has this accuracy lost relative to real time. There is also a bigger problem: Date.time is synced with clock so it is unstable. Given your sample lap ranges of a few seconds, you may even get negative lap.

Using a different timer designed for timing purpose, Performance.now, can minimize errors and solve the time bending magic.

like image 99
Sheepy Avatar answered Nov 15 '22 14:11

Sheepy


It is not possible to achieve what you want. You basically expect to get the same result when you round two numbers and add them, and when you first add the numbers and then round them.

Unfortunately, it doesn't work that way. For example, Math.round(0.4) + Math.round(0.4) gives 0, but Math.round(0.4 + 0.4) gives 1.

The only way to make the numbers add up correctly is to display three decimal places.


You could have a little more accurate results using the solution from the (now deleted) answer by Gerardo Furtado – that is, to use Math.round() to round the number, instead of cutting of the third digit, but that still wouldn't work in some cases.

like image 39
Michał Perłakowski Avatar answered Nov 15 '22 12:11

Michał Perłakowski


The issue your having is because your dropping precision by only going to centiseconds when you format the time. The way you first had it (without Math.round()) essentially does Math.floor by just trimming the last character off. So either way you're loosing precision. If you want to display only down to centiseconds and you want the math that the user sees to work, you could do the addition on the formatted amounts instead of the raw amounts like so:

// this just does the work of adding up the individuals after they've been formatted
const individualAdder = timeFormatter(individualTimes.reduce((total, time) => {
    return total + parseFloat(time.replace(/[^0-9]/g, ""));
}, 0) * 10);

/**
 * 00 : 27 . 12
 */
console.log(individualAdder);

You could also display individual times at full millisecond accuracy, depending on your desired experience.

like image 39
Ben Sidelinger Avatar answered Nov 15 '22 12:11

Ben Sidelinger