You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2022 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { MetricKey } from '../types/metrics';
  21. import {
  22. QualityGateStatusCondition,
  23. QualityGateStatusConditionEnhanced
  24. } from '../types/quality-gates';
  25. import { Dict, Measure, MeasureEnhanced, Metric } from '../types/types';
  26. import { getCurrentLocale, translate, translateWithParameters } from './l10n';
  27. import { isDefined } from './types';
  28. export function enhanceMeasuresWithMetrics(
  29. measures: Measure[],
  30. metrics: Metric[]
  31. ): MeasureEnhanced[] {
  32. return measures
  33. .map(measure => {
  34. const metric = metrics.find(metric => metric.key === measure.metric);
  35. return metric && { ...measure, metric };
  36. })
  37. .filter(isDefined);
  38. }
  39. export function enhanceConditionWithMeasure(
  40. condition: QualityGateStatusCondition,
  41. measures: MeasureEnhanced[]
  42. ): QualityGateStatusConditionEnhanced | undefined {
  43. const measure = measures.find(m => m.metric.key === condition.metric);
  44. // Make sure we have a period index. This is necessary when dealing with
  45. // applications.
  46. let { period } = condition;
  47. if (measure && measure.period && !period) {
  48. period = measure.period.index;
  49. }
  50. return measure && { ...condition, period, measure };
  51. }
  52. export function isPeriodBestValue(measure: Measure | MeasureEnhanced): boolean {
  53. return measure.period?.bestValue || false;
  54. }
  55. /** Check if metric is differential */
  56. export function isDiffMetric(metricKey: MetricKey | string): boolean {
  57. return metricKey.indexOf('new_') === 0;
  58. }
  59. export function getDisplayMetrics(metrics: Metric[]) {
  60. return metrics.filter(metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type));
  61. }
  62. export function findMeasure(measures: MeasureEnhanced[], metric: MetricKey | string) {
  63. return measures.find(measure => measure.metric.key === metric);
  64. }
  65. const HOURS_IN_DAY = 8;
  66. interface Formatter {
  67. (value: string | number, options?: any): string;
  68. }
  69. /** Format a measure value for a given type */
  70. export function formatMeasure(
  71. value: string | number | undefined,
  72. type: string,
  73. options?: any
  74. ): string {
  75. const formatter = getFormatter(type);
  76. // eslint-disable-next-line react-hooks/rules-of-hooks
  77. return useFormatter(value, formatter, options);
  78. }
  79. /** Return a localized metric name */
  80. export function localizeMetric(metricKey: string): string {
  81. return translate('metric', metricKey, 'name');
  82. }
  83. /** Return corresponding "short" for better display in UI */
  84. export function getShortType(type: string): string {
  85. if (type === 'INT') {
  86. return 'SHORT_INT';
  87. } else if (type === 'WORK_DUR') {
  88. return 'SHORT_WORK_DUR';
  89. }
  90. return type;
  91. }
  92. /*
  93. * Conditional decimal count for QualityGate-impacting measures
  94. * (e.g. Coverage %)
  95. * Increase the number of decimals if the value is close to the threshold
  96. * We count the precision (number of 0's, i.e. log10 and round down) needed to show the difference
  97. * E.g. threshold 85, value 84.9993 -> delta = 0.0007, we need 4 decimals to see the difference
  98. * otherwise rounding will make it look like they are equal.
  99. */
  100. const DEFAULT_DECIMALS = 1;
  101. export function getMinDecimalsCountToBeDistinctFromThreshold(
  102. value: number,
  103. threshold: number | undefined
  104. ): number {
  105. if (!threshold) {
  106. return DEFAULT_DECIMALS;
  107. }
  108. const delta = Math.abs(threshold - value);
  109. if (delta < 0.1 && delta > 0) {
  110. return -Math.floor(Math.log10(delta));
  111. }
  112. return DEFAULT_DECIMALS;
  113. }
  114. function useFormatter(
  115. value: string | number | undefined,
  116. formatter: Formatter,
  117. options?: any
  118. ): string {
  119. return value !== undefined && value !== '' ? formatter(value, options) : '';
  120. }
  121. function getFormatter(type: string): Formatter {
  122. const FORMATTERS: Dict<Formatter> = {
  123. INT: intFormatter,
  124. SHORT_INT: shortIntFormatter,
  125. FLOAT: floatFormatter,
  126. PERCENT: percentFormatter,
  127. WORK_DUR: durationFormatter,
  128. SHORT_WORK_DUR: shortDurationFormatter,
  129. RATING: ratingFormatter,
  130. LEVEL: levelFormatter,
  131. MILLISEC: millisecondsFormatter
  132. };
  133. return FORMATTERS[type] || noFormatter;
  134. }
  135. function numberFormatter(
  136. value: string | number,
  137. minimumFractionDigits = 0,
  138. maximumFractionDigits = minimumFractionDigits
  139. ) {
  140. const { format } = new Intl.NumberFormat(getCurrentLocale(), {
  141. minimumFractionDigits,
  142. maximumFractionDigits
  143. });
  144. if (typeof value === 'string') {
  145. return format(parseFloat(value));
  146. }
  147. return format(value);
  148. }
  149. function noFormatter(value: string | number): string | number {
  150. return value;
  151. }
  152. function intFormatter(value: string | number): string {
  153. return numberFormatter(value);
  154. }
  155. const shortIntFormats = [
  156. { unit: 1e10, formatUnit: 1e9, fraction: 0, suffix: 'short_number_suffix.g' },
  157. { unit: 1e9, formatUnit: 1e9, fraction: 1, suffix: 'short_number_suffix.g' },
  158. { unit: 1e7, formatUnit: 1e6, fraction: 0, suffix: 'short_number_suffix.m' },
  159. { unit: 1e6, formatUnit: 1e6, fraction: 1, suffix: 'short_number_suffix.m' },
  160. { unit: 1e4, formatUnit: 1e3, fraction: 0, suffix: 'short_number_suffix.k' },
  161. { unit: 1e3, formatUnit: 1e3, fraction: 1, suffix: 'short_number_suffix.k' }
  162. ];
  163. function shortIntFormatter(
  164. value: string | number,
  165. option?: { roundingFunc?: (x: number) => number }
  166. ): string {
  167. const roundingFunc = (option && option.roundingFunc) || undefined;
  168. if (typeof value === 'string') {
  169. value = parseFloat(value);
  170. }
  171. for (let i = 0; i < shortIntFormats.length; i++) {
  172. const { unit, formatUnit, fraction, suffix } = shortIntFormats[i];
  173. const nextFraction = unit / (shortIntFormats[i + 1] ? shortIntFormats[i + 1].unit / 10 : 1);
  174. const roundedValue = numberRound(value / unit, nextFraction, roundingFunc);
  175. if (roundedValue >= 1) {
  176. return (
  177. numberFormatter(
  178. numberRound(value / formatUnit, Math.pow(10, fraction), roundingFunc),
  179. 0,
  180. fraction
  181. ) + translate(suffix)
  182. );
  183. }
  184. }
  185. return numberFormatter(value);
  186. }
  187. function numberRound(
  188. value: number,
  189. fraction: number = 1000,
  190. roundingFunc: (x: number) => number = Math.round
  191. ) {
  192. return roundingFunc(value * fraction) / fraction;
  193. }
  194. function floatFormatter(value: string | number): string {
  195. return numberFormatter(value, 1, 5);
  196. }
  197. function percentFormatter(
  198. value: string | number,
  199. { decimals, omitExtraDecimalZeros }: { decimals?: number; omitExtraDecimalZeros?: boolean } = {}
  200. ): string {
  201. if (typeof value === 'string') {
  202. value = parseFloat(value);
  203. }
  204. if (value === 100) {
  205. return '100%';
  206. } else if (omitExtraDecimalZeros && decimals) {
  207. // If omitExtraDecimalZeros is true, all trailing decimal 0s will be removed,
  208. // except for the first decimal.
  209. // E.g. for decimals=3:
  210. // - omitExtraDecimalZeros: false, value: 45.450 => 45.450
  211. // - omitExtraDecimalZeros: true, value: 45.450 => 45.45
  212. // - omitExtraDecimalZeros: false, value: 85 => 85.000
  213. // - omitExtraDecimalZeros: true, value: 85 => 85.0
  214. return `${numberFormatter(value, 1, decimals)}%`;
  215. }
  216. return `${numberFormatter(value, decimals || 1)}%`;
  217. }
  218. function ratingFormatter(value: string | number): string {
  219. if (typeof value === 'string') {
  220. value = parseInt(value, 10);
  221. }
  222. return String.fromCharCode(97 + value - 1).toUpperCase();
  223. }
  224. function levelFormatter(value: string | number): string {
  225. if (typeof value === 'number') {
  226. value = value.toString();
  227. }
  228. const l10nKey = `metric.level.${value}`;
  229. const result = translate(l10nKey);
  230. // if couldn't translate, return the initial value
  231. return l10nKey !== result ? result : value;
  232. }
  233. function millisecondsFormatter(value: string | number): string {
  234. if (typeof value === 'string') {
  235. value = parseInt(value, 10);
  236. }
  237. const ONE_SECOND = 1000;
  238. const ONE_MINUTE = 60 * ONE_SECOND;
  239. if (value >= ONE_MINUTE) {
  240. const minutes = Math.round(value / ONE_MINUTE);
  241. return `${minutes}min`;
  242. } else if (value >= ONE_SECOND) {
  243. const seconds = Math.round(value / ONE_SECOND);
  244. return `${seconds}s`;
  245. }
  246. return `${value}ms`;
  247. }
  248. /*
  249. * Debt Formatters
  250. */
  251. function shouldDisplayDays(days: number): boolean {
  252. return days > 0;
  253. }
  254. function shouldDisplayDaysInShortFormat(days: number): boolean {
  255. return days > 0.9;
  256. }
  257. function shouldDisplayHours(days: number, hours: number): boolean {
  258. return hours > 0 && days < 10;
  259. }
  260. function shouldDisplayHoursInShortFormat(hours: number): boolean {
  261. return hours > 0.9;
  262. }
  263. function shouldDisplayMinutes(days: number, hours: number, minutes: number): boolean {
  264. return minutes > 0 && hours < 10 && days === 0;
  265. }
  266. function addSpaceIfNeeded(value: string): string {
  267. return value.length > 0 ? `${value} ` : value;
  268. }
  269. function formatDuration(isNegative: boolean, days: number, hours: number, minutes: number): string {
  270. let formatted = '';
  271. if (shouldDisplayDays(days)) {
  272. formatted += translateWithParameters('work_duration.x_days', isNegative ? -1 * days : days);
  273. }
  274. if (shouldDisplayHours(days, hours)) {
  275. formatted = addSpaceIfNeeded(formatted);
  276. formatted += translateWithParameters(
  277. 'work_duration.x_hours',
  278. isNegative && formatted.length === 0 ? -1 * hours : hours
  279. );
  280. }
  281. if (shouldDisplayMinutes(days, hours, minutes)) {
  282. formatted = addSpaceIfNeeded(formatted);
  283. formatted += translateWithParameters(
  284. 'work_duration.x_minutes',
  285. isNegative && formatted.length === 0 ? -1 * minutes : minutes
  286. );
  287. }
  288. return formatted;
  289. }
  290. function formatDurationShort(
  291. isNegative: boolean,
  292. days: number,
  293. hours: number,
  294. minutes: number
  295. ): string {
  296. if (shouldDisplayDaysInShortFormat(days)) {
  297. const roundedDays = Math.round(days);
  298. const formattedDays = formatMeasure(isNegative ? -1 * roundedDays : roundedDays, 'SHORT_INT');
  299. return translateWithParameters('work_duration.x_days', formattedDays);
  300. }
  301. if (shouldDisplayHoursInShortFormat(hours)) {
  302. const roundedHours = Math.round(hours);
  303. const formattedHours = formatMeasure(
  304. isNegative ? -1 * roundedHours : roundedHours,
  305. 'SHORT_INT'
  306. );
  307. return translateWithParameters('work_duration.x_hours', formattedHours);
  308. }
  309. const formattedMinutes = formatMeasure(isNegative ? -1 * minutes : minutes, 'SHORT_INT');
  310. return translateWithParameters('work_duration.x_minutes', formattedMinutes);
  311. }
  312. function durationFormatter(value: string | number): string {
  313. if (typeof value === 'string') {
  314. value = parseInt(value, 10);
  315. }
  316. if (value === 0) {
  317. return '0';
  318. }
  319. const hoursInDay = HOURS_IN_DAY;
  320. const isNegative = value < 0;
  321. const absValue = Math.abs(value);
  322. const days = Math.floor(absValue / hoursInDay / 60);
  323. let remainingValue = absValue - days * hoursInDay * 60;
  324. const hours = Math.floor(remainingValue / 60);
  325. remainingValue -= hours * 60;
  326. return formatDuration(isNegative, days, hours, remainingValue);
  327. }
  328. function shortDurationFormatter(value: string | number): string {
  329. if (typeof value === 'string') {
  330. value = parseInt(value, 10);
  331. }
  332. if (value === 0) {
  333. return '0';
  334. }
  335. const hoursInDay = HOURS_IN_DAY;
  336. const isNegative = value < 0;
  337. const absValue = Math.abs(value);
  338. const days = absValue / hoursInDay / 60;
  339. let remainingValue = absValue - Math.floor(days) * hoursInDay * 60;
  340. const hours = remainingValue / 60;
  341. remainingValue -= Math.floor(hours) * 60;
  342. return formatDurationShort(isNegative, days, hours, remainingValue);
  343. }