Форматирование таймеров без потери точности?

У меня есть массив времени начала/остановки. Я в основном хочу отображать время, которое требуется для каждой записи, а также общее время для всех из них. Вот код, который я написал, чтобы попытаться сделать это:

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

Ответ 1

Проблема

Вы теряете точность с каждым отображением округленного времени. Чем больше кругов у вас, тем больше проблема может быть:

╔══════╦════════════════════════════════╗
║ 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 time = 12189
  • Фактическое время = 12189 - 12193
  • Отображаемое время = 12180 to 12240

Другими словами, путем добавления отображаемого времени также добавляется потерянная отображаемая точность. Неспособность учитывать это, потерянное человеком, является реальной проблемой.

Решение

  • Увеличьте отображаемую точность (если вы не округлите цифры, вы ничего не потеряете)
  • Используйте отображаемую сумму, которая менее точна, но не является неправильной, если вы также указали допуск (±).
  • Обнаружение проблемы и отображение сообщения.

Ниже приведена демонстрация решения 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>

Ответ 2

Невозможно достичь того, чего вы хотите. В основном вы ожидаете получить тот же результат, когда вы округлите два числа и добавите их, и когда вы сначала добавите числа, а затем округлите их.

К сожалению, это не работает. Например, Math.round(0.4) + Math.round(0.4) дает 0, но Math.round(0.4 + 0.4) дает 1.

Единственный способ правильно сделать цифры - отобразить три десятичных знака.


Вы могли бы получить более точные результаты, используя решение из (теперь удалённого) ответа Херардо Фуртадо. – то есть использовать Math.round() для округления числа вместо сокращения третьей цифры, но в некоторых случаях это все равно не будет работать.

Ответ 3

Проблема, связанная с тем, что ваша проблема заключается в том, что вы снижаете точность, только когда вы отформатируете время в секундах. То, как вы впервые это сделали (без Math.round()), по существу делает Math.floor, просто отключив последний символ. Так или иначе вы теряете точность. Если вы хотите отображать только до 100 секунд, и вы хотите, чтобы математика, которую пользователь видит, работает, вы можете сделать добавление в отформатированных количествах вместо необработанных сумм, например:

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

Вы также можете отображать отдельные моменты с полной точностью в миллисекундах в зависимости от желаемого опыта.

Ответ 4

Ваше решение обрезало последнюю значащую цифру.

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