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.

measures.ts 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2018 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 { translate, translateWithParameters, getCurrentLocale } from './l10n';
  21. const HOURS_IN_DAY = 8;
  22. interface Formatter {
  23. (value: string | number, options?: any): string;
  24. }
  25. /** Format a measure value for a given type */
  26. export function formatMeasure(
  27. value: string | number | undefined,
  28. type: string,
  29. options?: any
  30. ): string {
  31. const formatter = getFormatter(type);
  32. return useFormatter(value, formatter, options);
  33. }
  34. /** Return a localized metric name */
  35. export function localizeMetric(metricKey: string): string {
  36. return translate('metric', metricKey, 'name');
  37. }
  38. /** Return corresponding "short" for better display in UI */
  39. export function getShortType(type: string): string {
  40. if (type === 'INT') {
  41. return 'SHORT_INT';
  42. } else if (type === 'WORK_DUR') {
  43. return 'SHORT_WORK_DUR';
  44. }
  45. return type;
  46. }
  47. export function enhanceMeasuresWithMetrics(
  48. measures: T.Measure[],
  49. metrics: T.Metric[]
  50. ): T.MeasureEnhanced[] {
  51. return measures.map(measure => {
  52. const metric = metrics.find(metric => metric.key === measure.metric) as T.Metric;
  53. return { ...measure, metric };
  54. });
  55. }
  56. /** Get period value of a measure */
  57. export function getPeriodValue(
  58. measure: T.Measure | T.MeasureEnhanced,
  59. periodIndex: number
  60. ): string | undefined {
  61. const { periods } = measure;
  62. const period = periods && periods.find(period => period.index === periodIndex);
  63. return period ? period.value : undefined;
  64. }
  65. export function isPeriodBestValue(
  66. measure: T.Measure | T.MeasureEnhanced,
  67. periodIndex: number
  68. ): boolean {
  69. const { periods } = measure;
  70. const period = periods && periods.find(period => period.index === periodIndex);
  71. return (period && period.bestValue) || false;
  72. }
  73. /** Check if metric is differential */
  74. export function isDiffMetric(metricKey: string): boolean {
  75. return metricKey.indexOf('new_') === 0;
  76. }
  77. function useFormatter(
  78. value: string | number | undefined,
  79. formatter: Formatter,
  80. options?: any
  81. ): string {
  82. return value !== undefined && value !== '' ? formatter(value, options) : '';
  83. }
  84. function getFormatter(type: string): Formatter {
  85. const FORMATTERS: { [type: string]: Formatter } = {
  86. INT: intFormatter,
  87. SHORT_INT: shortIntFormatter,
  88. FLOAT: floatFormatter,
  89. PERCENT: percentFormatter,
  90. WORK_DUR: durationFormatter,
  91. SHORT_WORK_DUR: shortDurationFormatter,
  92. RATING: ratingFormatter,
  93. LEVEL: levelFormatter,
  94. MILLISEC: millisecondsFormatter
  95. };
  96. return FORMATTERS[type] || noFormatter;
  97. }
  98. function numberFormatter(
  99. value: number,
  100. minimumFractionDigits = 0,
  101. maximumFractionDigits = minimumFractionDigits
  102. ) {
  103. const { format } = new Intl.NumberFormat(getCurrentLocale(), {
  104. minimumFractionDigits,
  105. maximumFractionDigits
  106. });
  107. return format(value);
  108. }
  109. function noFormatter(value: string | number): string | number {
  110. return value;
  111. }
  112. function intFormatter(value: number): string {
  113. return numberFormatter(value);
  114. }
  115. function shortIntFormatter(value: number): string {
  116. if (value >= 1e9) {
  117. return numberFormatter(value / 1e9) + translate('short_number_suffix.g');
  118. } else if (value >= 1e6) {
  119. return numberFormatter(value / 1e6) + translate('short_number_suffix.m');
  120. } else if (value >= 1e4) {
  121. return numberFormatter(value / 1e3) + translate('short_number_suffix.k');
  122. } else if (value >= 1e3) {
  123. return numberFormatter(value / 1e3, 0, 1) + translate('short_number_suffix.k');
  124. } else {
  125. return numberFormatter(value);
  126. }
  127. }
  128. function floatFormatter(value: number): string {
  129. return numberFormatter(value, 1, 5);
  130. }
  131. function percentFormatter(value: string | number, options: { decimals?: number } = {}): string {
  132. if (typeof value === 'string') {
  133. value = parseFloat(value);
  134. }
  135. if (options.decimals) {
  136. return numberFormatter(value, options.decimals) + '%';
  137. }
  138. return value === 100 ? '100%' : numberFormatter(value, 1) + '%';
  139. }
  140. function ratingFormatter(value: string | number): string {
  141. if (typeof value === 'string') {
  142. value = parseInt(value, 10);
  143. }
  144. return String.fromCharCode(97 + value - 1).toUpperCase();
  145. }
  146. function levelFormatter(value: string): string {
  147. const l10nKey = 'metric.level.' + value;
  148. const result = translate(l10nKey);
  149. // if couldn't translate, return the initial value
  150. return l10nKey !== result ? result : value;
  151. }
  152. function millisecondsFormatter(value: number): string {
  153. const ONE_SECOND = 1000;
  154. const ONE_MINUTE = 60 * ONE_SECOND;
  155. if (value >= ONE_MINUTE) {
  156. const minutes = Math.round(value / ONE_MINUTE);
  157. return `${minutes}min`;
  158. } else if (value >= ONE_SECOND) {
  159. const seconds = Math.round(value / ONE_SECOND);
  160. return `${seconds}s`;
  161. } else {
  162. return `${value}ms`;
  163. }
  164. }
  165. /*
  166. * Debt Formatters
  167. */
  168. function shouldDisplayDays(days: number): boolean {
  169. return days > 0;
  170. }
  171. function shouldDisplayDaysInShortFormat(days: number): boolean {
  172. return days > 0.9;
  173. }
  174. function shouldDisplayHours(days: number, hours: number): boolean {
  175. return hours > 0 && days < 10;
  176. }
  177. function shouldDisplayHoursInShortFormat(hours: number): boolean {
  178. return hours > 0.9;
  179. }
  180. function shouldDisplayMinutes(days: number, hours: number, minutes: number): boolean {
  181. return minutes > 0 && hours < 10 && days === 0;
  182. }
  183. function addSpaceIfNeeded(value: string): string {
  184. return value.length > 0 ? value + ' ' : value;
  185. }
  186. function formatDuration(isNegative: boolean, days: number, hours: number, minutes: number): string {
  187. let formatted = '';
  188. if (shouldDisplayDays(days)) {
  189. formatted += translateWithParameters('work_duration.x_days', isNegative ? -1 * days : days);
  190. }
  191. if (shouldDisplayHours(days, hours)) {
  192. formatted = addSpaceIfNeeded(formatted);
  193. formatted += translateWithParameters(
  194. 'work_duration.x_hours',
  195. isNegative && formatted.length === 0 ? -1 * hours : hours
  196. );
  197. }
  198. if (shouldDisplayMinutes(days, hours, minutes)) {
  199. formatted = addSpaceIfNeeded(formatted);
  200. formatted += translateWithParameters(
  201. 'work_duration.x_minutes',
  202. isNegative && formatted.length === 0 ? -1 * minutes : minutes
  203. );
  204. }
  205. return formatted;
  206. }
  207. function formatDurationShort(
  208. isNegative: boolean,
  209. days: number,
  210. hours: number,
  211. minutes: number
  212. ): string {
  213. if (shouldDisplayDaysInShortFormat(days)) {
  214. const roundedDays = Math.round(days);
  215. const formattedDays = formatMeasure(isNegative ? -1 * roundedDays : roundedDays, 'SHORT_INT');
  216. return translateWithParameters('work_duration.x_days', formattedDays);
  217. }
  218. if (shouldDisplayHoursInShortFormat(hours)) {
  219. const roundedHours = Math.round(hours);
  220. const formattedHours = formatMeasure(
  221. isNegative ? -1 * roundedHours : roundedHours,
  222. 'SHORT_INT'
  223. );
  224. return translateWithParameters('work_duration.x_hours', formattedHours);
  225. }
  226. const formattedMinutes = formatMeasure(isNegative ? -1 * minutes : minutes, 'SHORT_INT');
  227. return translateWithParameters('work_duration.x_minutes', formattedMinutes);
  228. }
  229. function durationFormatter(value: string | number): string {
  230. if (typeof value === 'string') {
  231. value = parseInt(value, 10);
  232. }
  233. if (value === 0) {
  234. return '0';
  235. }
  236. const hoursInDay = HOURS_IN_DAY;
  237. const isNegative = value < 0;
  238. const absValue = Math.abs(value);
  239. const days = Math.floor(absValue / hoursInDay / 60);
  240. let remainingValue = absValue - days * hoursInDay * 60;
  241. const hours = Math.floor(remainingValue / 60);
  242. remainingValue -= hours * 60;
  243. return formatDuration(isNegative, days, hours, remainingValue);
  244. }
  245. function shortDurationFormatter(value: string | number): string {
  246. if (typeof value === 'string') {
  247. value = parseInt(value, 10);
  248. }
  249. if (value === 0) {
  250. return '0';
  251. }
  252. const hoursInDay = HOURS_IN_DAY;
  253. const isNegative = value < 0;
  254. const absValue = Math.abs(value);
  255. const days = absValue / hoursInDay / 60;
  256. let remainingValue = absValue - Math.floor(days) * hoursInDay * 60;
  257. const hours = remainingValue / 60;
  258. remainingValue -= Math.floor(hours) * 60;
  259. return formatDurationShort(isNegative, days, hours, remainingValue);
  260. }
  261. function getRatingGrid(): string {
  262. // workaround cyclic dependencies
  263. const getStore = require('../app/utils/getStore').default;
  264. const { getGlobalSettingValue } = require('../store/rootReducer');
  265. const store = getStore();
  266. const settingValue = getGlobalSettingValue(store.getState(), 'sonar.technicalDebt.ratingGrid');
  267. return settingValue ? settingValue.value : '';
  268. }
  269. let maintainabilityRatingGrid: number[];
  270. function getMaintainabilityRatingGrid(): number[] {
  271. if (maintainabilityRatingGrid) {
  272. return maintainabilityRatingGrid;
  273. }
  274. const str = getRatingGrid();
  275. const numbers = str
  276. .split(',')
  277. .map(s => parseFloat(s))
  278. .filter(n => !isNaN(n));
  279. if (numbers.length === 4) {
  280. maintainabilityRatingGrid = numbers;
  281. } else {
  282. maintainabilityRatingGrid = [0, 0, 0, 0];
  283. }
  284. return maintainabilityRatingGrid;
  285. }
  286. function getMaintainabilityRatingTooltip(rating: number): string {
  287. const maintainabilityGrid = getMaintainabilityRatingGrid();
  288. const maintainabilityRatingThreshold = maintainabilityGrid[Math.floor(rating) - 2];
  289. if (rating < 2) {
  290. return translateWithParameters(
  291. 'metric.sqale_rating.tooltip.A',
  292. formatMeasure(maintainabilityGrid[0] * 100, 'PERCENT')
  293. );
  294. }
  295. const ratingLetter = formatMeasure(rating, 'RATING');
  296. return translateWithParameters(
  297. 'metric.sqale_rating.tooltip',
  298. ratingLetter,
  299. formatMeasure(maintainabilityRatingThreshold * 100, 'PERCENT')
  300. );
  301. }
  302. export function getRatingTooltip(metricKey: string, value: number | string): string {
  303. const ratingLetter = formatMeasure(value, 'RATING');
  304. const finalMetricKey = isDiffMetric(metricKey) ? metricKey.substr(4) : metricKey;
  305. return finalMetricKey === 'sqale_rating' || finalMetricKey === 'maintainability_rating'
  306. ? getMaintainabilityRatingTooltip(Number(value))
  307. : translate('metric', finalMetricKey, 'tooltip', ratingLetter);
  308. }
  309. export function getDisplayMetrics(metrics: T.Metric[]) {
  310. return metrics.filter(metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type));
  311. }