在不损失准确性的情况下格式化计时器?

Formatting timers without losing accuracy?

我有一个 start/stop 次数组。我基本上想显示每个条目花费的时间,以及所有条目的总时间。这是我为尝试执行此操作而编写的代码:

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);

但是,我正在失去准确性。如您所见,各个时间加起来不等于 mainTimer 值。无论时间如何,它总是关闭 0.01 - 0.03。

有什么方法可以确保时间只显示两个地方,但仍然正确相加?任何帮助将不胜感激。

我在 JSFiddle 上也有这个,它更容易 运行。


编辑:当前答案确实适用于我上面提供的案例,但它不适用于所有案例,例如 this 一个。

您遇到的问题是因为您在设置时间格式时仅将精度降低到厘秒。您最初拥有它的方式(没有 Math.round())基本上只是通过修剪最后一个字符来实现 Math.floor。所以无论哪种方式,你都会失去精度。如果您只想显示到厘秒,并且您希望用户看到的数学有效,您可以对格式化的金额而不是原始金额进行加法,如下所示:

// 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);

您还可以根据您想要的体验以完整的毫秒精度显示各个时间。

您的解决方案裁剪了最后一个有效数字。

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 = `00${milliseconds % 1000}`.slice(-3);  //changed

  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 . 601',
     *   '00 : 08 . 369',
     *   '00 : 06 . 975',
     *   '00 : 05 . 191'
     * ]
     */
console.log(individualTimes);

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

你想要的是不可能实现的。当您将两个数字四舍五入并相加时,以及当您首先将数字相加然后四舍五入时,您基本上期望得到相同的结果。

不幸的是,它不是那样工作的。例如,Math.round(0.4) + Math.round(0.4) 给出 0,但 Math.round(0.4 + 0.4) 给出 1.

使数字相加正确的唯一方法是显示三位小数。


使用来自(现已删除)answer by Gerardo Furtado 的解决方案,您可以获得更准确的结果 – 也就是说,使用 Math.round() 来舍入数字,而不是删除第三位数字, 但在某些情况下仍然行不通。

问题

您每次显示舍入时间都会失去准确性。 您的圈数越多,问题就越大:

╔══════╦════════════════════════════════╗
║ 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 ║
╚══════╩═══════╩═════════════╩══════════╝

考虑到公差后,不同的总数实际上相互重叠:

  • JS时间=12189
  • 实际时间 = 12189 到 12193
  • 显示时间 = 12180 到 12240

换句话说,将显示的时间加起来,也会加上显示精度的损失。 没有考虑到人类丢失的这个才是真正的问题。

解决方案

  1. 提高显示的精度(如果你不舍入数字,你不会丢失任何东西)
  2. 使用显示的总和,该总和不太准确,但如果您还说明了公差 (±),则不会不正确。
  3. 检测问题并显示消息。

这是解决方案 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>

或者忍受它

所有计时器,甚至物理计时器,都必须忍受这个舍入问题。 你见过有这样免责声明的计时器吗?

对于一个计时器来说,总时间显示得越准确,即使不一致,也肯定是越正确的。

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.