index.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import parseMilliseconds from 'parse-ms';
  2. const isZero = value => value === 0 || value === 0n;
  3. const pluralize = (word, count) => (count === 1 || count === 1n) ? word : `${word}s`;
  4. const SECOND_ROUNDING_EPSILON = 0.000_000_1;
  5. const ONE_DAY_IN_MILLISECONDS = 24n * 60n * 60n * 1000n;
  6. export default function prettyMilliseconds(milliseconds, options) {
  7. const isBigInt = typeof milliseconds === 'bigint';
  8. if (!isBigInt && !Number.isFinite(milliseconds)) {
  9. throw new TypeError('Expected a finite number or bigint');
  10. }
  11. options = {...options};
  12. const sign = milliseconds < 0 ? '-' : '';
  13. milliseconds = milliseconds < 0 ? -milliseconds : milliseconds; // Cannot use `Math.abs()` because of BigInt support.
  14. if (options.colonNotation) {
  15. options.compact = false;
  16. options.formatSubMilliseconds = false;
  17. options.separateMilliseconds = false;
  18. options.verbose = false;
  19. }
  20. if (options.compact) {
  21. options.unitCount = 1;
  22. options.secondsDecimalDigits = 0;
  23. options.millisecondsDecimalDigits = 0;
  24. }
  25. let result = [];
  26. const floorDecimals = (value, decimalDigits) => {
  27. const flooredInterimValue = Math.floor((value * (10 ** decimalDigits)) + SECOND_ROUNDING_EPSILON);
  28. const flooredValue = Math.round(flooredInterimValue) / (10 ** decimalDigits);
  29. return flooredValue.toFixed(decimalDigits);
  30. };
  31. const add = (value, long, short, valueString) => {
  32. if (
  33. (result.length === 0 || !options.colonNotation)
  34. && isZero(value)
  35. && !(options.colonNotation && short === 'm')) {
  36. return;
  37. }
  38. valueString ??= String(value);
  39. if (options.colonNotation) {
  40. const wholeDigits = valueString.includes('.') ? valueString.split('.')[0].length : valueString.length;
  41. const minLength = result.length > 0 ? 2 : 1;
  42. valueString = '0'.repeat(Math.max(0, minLength - wholeDigits)) + valueString;
  43. } else {
  44. valueString += options.verbose ? ' ' + pluralize(long, value) : short;
  45. }
  46. result.push(valueString);
  47. };
  48. const parsed = parseMilliseconds(milliseconds);
  49. const days = BigInt(parsed.days);
  50. if (options.hideYearAndDays) {
  51. add((BigInt(days) * 24n) + BigInt(parsed.hours), 'hour', 'h');
  52. } else {
  53. if (options.hideYear) {
  54. add(days, 'day', 'd');
  55. } else {
  56. add(days / 365n, 'year', 'y');
  57. add(days % 365n, 'day', 'd');
  58. }
  59. add(Number(parsed.hours), 'hour', 'h');
  60. }
  61. add(Number(parsed.minutes), 'minute', 'm');
  62. if (!options.hideSeconds) {
  63. if (
  64. options.separateMilliseconds
  65. || options.formatSubMilliseconds
  66. || (!options.colonNotation && milliseconds < 1000)
  67. ) {
  68. const seconds = Number(parsed.seconds);
  69. const milliseconds = Number(parsed.milliseconds);
  70. const microseconds = Number(parsed.microseconds);
  71. const nanoseconds = Number(parsed.nanoseconds);
  72. add(seconds, 'second', 's');
  73. if (options.formatSubMilliseconds) {
  74. add(milliseconds, 'millisecond', 'ms');
  75. add(microseconds, 'microsecond', 'µs');
  76. add(nanoseconds, 'nanosecond', 'ns');
  77. } else {
  78. const millisecondsAndBelow
  79. = milliseconds
  80. + (microseconds / 1000)
  81. + (nanoseconds / 1e6);
  82. const millisecondsDecimalDigits
  83. = typeof options.millisecondsDecimalDigits === 'number'
  84. ? options.millisecondsDecimalDigits
  85. : 0;
  86. const roundedMilliseconds = millisecondsAndBelow >= 1
  87. ? Math.round(millisecondsAndBelow)
  88. : Math.ceil(millisecondsAndBelow);
  89. const millisecondsString = millisecondsDecimalDigits
  90. ? millisecondsAndBelow.toFixed(millisecondsDecimalDigits)
  91. : roundedMilliseconds;
  92. add(
  93. Number.parseFloat(millisecondsString),
  94. 'millisecond',
  95. 'ms',
  96. millisecondsString,
  97. );
  98. }
  99. } else {
  100. const seconds = (
  101. (isBigInt ? Number(milliseconds % ONE_DAY_IN_MILLISECONDS) : milliseconds)
  102. / 1000
  103. ) % 60;
  104. const secondsDecimalDigits
  105. = typeof options.secondsDecimalDigits === 'number'
  106. ? options.secondsDecimalDigits
  107. : 1;
  108. const secondsFixed = floorDecimals(seconds, secondsDecimalDigits);
  109. const secondsString = options.keepDecimalsOnWholeSeconds
  110. ? secondsFixed
  111. : secondsFixed.replace(/\.0+$/, '');
  112. add(Number.parseFloat(secondsString), 'second', 's', secondsString);
  113. }
  114. }
  115. if (result.length === 0) {
  116. return sign + '0' + (options.verbose ? ' milliseconds' : 'ms');
  117. }
  118. const separator = options.colonNotation ? ':' : ' ';
  119. if (typeof options.unitCount === 'number') {
  120. result = result.slice(0, Math.max(options.unitCount, 1));
  121. }
  122. return sign + result.join(separator);
  123. }