formatters.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import {Release} from '@sentry/release-parser';
  2. import round from 'lodash/round';
  3. import {t, tn} from 'sentry/locale';
  4. import {CommitAuthor, User} from 'sentry/types';
  5. import {RATE_UNIT_LABELS, RateUnits} from 'sentry/utils/discover/fields';
  6. export function userDisplayName(user: User | CommitAuthor, includeEmail = true): string {
  7. let displayName = String(user?.name ?? t('Unknown author')).trim();
  8. if (displayName.length <= 0) {
  9. displayName = t('Unknown author');
  10. }
  11. const email = String(user?.email ?? '').trim();
  12. if (email.length > 0 && email !== displayName && includeEmail) {
  13. displayName += ' (' + email + ')';
  14. }
  15. return displayName;
  16. }
  17. export const isSemverRelease = (rawVersion: string): boolean => {
  18. try {
  19. const parsedVersion = new Release(rawVersion);
  20. return !!parsedVersion.versionParsed;
  21. } catch {
  22. return false;
  23. }
  24. };
  25. export const formatVersion = (rawVersion: string, withPackage = false) => {
  26. try {
  27. const parsedVersion = new Release(rawVersion);
  28. const versionToDisplay = parsedVersion.describe();
  29. if (versionToDisplay.length) {
  30. return `${versionToDisplay}${
  31. withPackage && parsedVersion.package ? `, ${parsedVersion.package}` : ''
  32. }`;
  33. }
  34. return rawVersion;
  35. } catch {
  36. return rawVersion;
  37. }
  38. };
  39. function roundWithFixed(
  40. value: number,
  41. fixedDigits: number
  42. ): {label: string; result: number} {
  43. const label = value.toFixed(fixedDigits);
  44. const result = fixedDigits <= 0 ? Math.round(value) : value;
  45. return {label, result};
  46. }
  47. // in milliseconds
  48. export const MONTH = 2629800000;
  49. export const WEEK = 604800000;
  50. export const DAY = 86400000;
  51. export const HOUR = 3600000;
  52. export const MINUTE = 60000;
  53. export const SECOND = 1000;
  54. /**
  55. * Returns a human redable duration rounded to the largest unit.
  56. *
  57. * e.g. 2 days, or 3 months, or 25 seoconds
  58. *
  59. * Use `getExactDuration` for exact durations
  60. */
  61. export function getDuration(
  62. seconds: number,
  63. fixedDigits: number = 0,
  64. abbreviation: boolean = false,
  65. extraShort: boolean = false,
  66. absolute: boolean = false
  67. ): string {
  68. const absValue = Math.abs(seconds * 1000);
  69. // value in milliseconds
  70. const msValue = absolute ? absValue : seconds * 1000;
  71. if (absValue >= MONTH && !extraShort) {
  72. const {label, result} = roundWithFixed(msValue / MONTH, fixedDigits);
  73. return `${label}${abbreviation ? t('mo') : ` ${tn('month', 'months', result)}`}`;
  74. }
  75. if (absValue >= WEEK) {
  76. const {label, result} = roundWithFixed(msValue / WEEK, fixedDigits);
  77. if (extraShort) {
  78. return `${label}${t('w')}`;
  79. }
  80. if (abbreviation) {
  81. return `${label}${t('wk')}`;
  82. }
  83. return `${label} ${tn('week', 'weeks', result)}`;
  84. }
  85. if (absValue >= DAY) {
  86. const {label, result} = roundWithFixed(msValue / DAY, fixedDigits);
  87. if (extraShort || abbreviation) {
  88. return `${label}${t('d')}`;
  89. }
  90. return `${label} ${tn('day', 'days', result)}`;
  91. }
  92. if (absValue >= HOUR) {
  93. const {label, result} = roundWithFixed(msValue / HOUR, fixedDigits);
  94. if (extraShort) {
  95. return `${label}${t('h')}`;
  96. }
  97. if (abbreviation) {
  98. return `${label}${t('hr')}`;
  99. }
  100. return `${label} ${tn('hour', 'hours', result)}`;
  101. }
  102. if (absValue >= MINUTE) {
  103. const {label, result} = roundWithFixed(msValue / MINUTE, fixedDigits);
  104. if (extraShort) {
  105. return `${label}${t('m')}`;
  106. }
  107. if (abbreviation) {
  108. return `${label}${t('min')}`;
  109. }
  110. return `${label} ${tn('minute', 'minutes', result)}`;
  111. }
  112. if (absValue >= SECOND) {
  113. const {label, result} = roundWithFixed(msValue / SECOND, fixedDigits);
  114. if (extraShort || abbreviation) {
  115. return `${label}${t('s')}`;
  116. }
  117. return `${label} ${tn('second', 'seconds', result)}`;
  118. }
  119. const {label, result} = roundWithFixed(msValue, fixedDigits);
  120. if (extraShort || abbreviation) {
  121. return `${label}${t('ms')}`;
  122. }
  123. return `${label} ${tn('millisecond', 'milliseconds', result)}`;
  124. }
  125. /**
  126. * Returns a human readable exact duration.
  127. *
  128. * e.g. 1 hour 25 minutes 15 seconds
  129. */
  130. export function getExactDuration(seconds: number, abbreviation: boolean = false) {
  131. const convertDuration = (secs: number, abbr: boolean): string => {
  132. // value in milliseconds
  133. const msValue = round(secs * 1000);
  134. const value = round(Math.abs(secs * 1000));
  135. const divideBy = (time: number) => {
  136. return {
  137. quotient: msValue < 0 ? Math.ceil(msValue / time) : Math.floor(msValue / time),
  138. remainder: msValue % time,
  139. };
  140. };
  141. if (value >= WEEK) {
  142. const {quotient, remainder} = divideBy(WEEK);
  143. const suffix = abbr ? t('wk') : ` ${tn('week', 'weeks', quotient)}`;
  144. return `${quotient}${suffix} ${convertDuration(remainder / 1000, abbr)}`;
  145. }
  146. if (value >= DAY) {
  147. const {quotient, remainder} = divideBy(DAY);
  148. const suffix = abbr ? t('d') : ` ${tn('day', 'days', quotient)}`;
  149. return `${quotient}${suffix} ${convertDuration(remainder / 1000, abbr)}`;
  150. }
  151. if (value >= HOUR) {
  152. const {quotient, remainder} = divideBy(HOUR);
  153. const suffix = abbr ? t('hr') : ` ${tn('hour', 'hours', quotient)}`;
  154. return `${quotient}${suffix} ${convertDuration(remainder / 1000, abbr)}`;
  155. }
  156. if (value >= MINUTE) {
  157. const {quotient, remainder} = divideBy(MINUTE);
  158. const suffix = abbr ? t('min') : ` ${tn('minute', 'minutes', quotient)}`;
  159. return `${quotient}${suffix} ${convertDuration(remainder / 1000, abbr)}`;
  160. }
  161. if (value >= SECOND) {
  162. const {quotient, remainder} = divideBy(SECOND);
  163. const suffix = abbr ? t('s') : ` ${tn('second', 'seconds', quotient)}`;
  164. return `${quotient}${suffix} ${convertDuration(remainder / 1000, abbr)}`;
  165. }
  166. if (value === 0) {
  167. return '';
  168. }
  169. const suffix = abbr ? t('ms') : ` ${tn('millisecond', 'milliseconds', value)}`;
  170. return `${msValue}${suffix}`;
  171. };
  172. const result = convertDuration(seconds, abbreviation).trim();
  173. if (result.length) {
  174. return result;
  175. }
  176. return `0${abbreviation ? t('ms') : ` ${t('milliseconds')}`}`;
  177. }
  178. export function formatSecondsToClock(
  179. seconds: number,
  180. {padAll}: {padAll: boolean} = {padAll: true}
  181. ) {
  182. if (seconds === 0 || isNaN(seconds)) {
  183. return padAll ? '00:00' : '0:00';
  184. }
  185. const divideBy = (msValue: number, time: number) => {
  186. return {
  187. quotient: msValue < 0 ? Math.ceil(msValue / time) : Math.floor(msValue / time),
  188. remainder: msValue % time,
  189. };
  190. };
  191. // value in milliseconds
  192. const absMSValue = round(Math.abs(seconds * 1000));
  193. const {quotient: hours, remainder: rMins} = divideBy(absMSValue, HOUR);
  194. const {quotient: minutes, remainder: rSeconds} = divideBy(rMins, MINUTE);
  195. const {quotient: secs, remainder: milliseconds} = divideBy(rSeconds, SECOND);
  196. const fill = (num: number) => (num < 10 ? `0${num}` : String(num));
  197. const parts = hours
  198. ? [padAll ? fill(hours) : hours, fill(minutes), fill(secs)]
  199. : [padAll ? fill(minutes) : minutes, fill(secs)];
  200. const ms = `000${milliseconds}`.slice(-3);
  201. return milliseconds ? `${parts.join(':')}.${ms}` : parts.join(':');
  202. }
  203. export function parseClockToSeconds(clock: string) {
  204. const [rest, milliseconds] = clock.split('.');
  205. const parts = rest.split(':');
  206. let seconds = 0;
  207. const progression = [MONTH, WEEK, DAY, HOUR, MINUTE, SECOND].slice(parts.length * -1);
  208. for (let i = 0; i < parts.length; i++) {
  209. const num = Number(parts[i]) || 0;
  210. const time = progression[i] / 1000;
  211. seconds += num * time;
  212. }
  213. const ms = Number(milliseconds) || 0;
  214. return seconds + ms / 1000;
  215. }
  216. export function formatFloat(number: number, places: number) {
  217. const multi = Math.pow(10, places);
  218. return parseInt((number * multi).toString(), 10) / multi;
  219. }
  220. /**
  221. * Format a value between 0 and 1 as a percentage
  222. */
  223. export function formatPercentage(value: number, places: number = 2) {
  224. if (value === 0) {
  225. return '0%';
  226. }
  227. return (
  228. round(value * 100, places).toLocaleString(undefined, {
  229. maximumFractionDigits: places,
  230. }) + '%'
  231. );
  232. }
  233. const numberFormats = [
  234. [1000000000, 'b'],
  235. [1000000, 'm'],
  236. [1000, 'k'],
  237. ] as const;
  238. export function formatAbbreviatedNumber(number: number | string) {
  239. number = Number(number);
  240. let lookup: (typeof numberFormats)[number];
  241. // eslint-disable-next-line no-cond-assign
  242. for (let i = 0; (lookup = numberFormats[i]); i++) {
  243. const [suffixNum, suffix] = lookup;
  244. const shortValue = Math.floor(number / suffixNum);
  245. const fitsBound = number % suffixNum;
  246. if (shortValue <= 0) {
  247. continue;
  248. }
  249. return shortValue / 10 > 1 || !fitsBound
  250. ? `${shortValue}${suffix}`
  251. : `${formatFloat(number / suffixNum, 1)}${suffix}`;
  252. }
  253. return number.toLocaleString();
  254. }
  255. export function formatRate(value: number, unit: RateUnits = RateUnits.PER_SECOND) {
  256. return `${formatAbbreviatedNumber(value)}${RATE_UNIT_LABELS[unit]}`;
  257. }