import { QualityGateIndicator } from 'design-system';
import React from 'react';
import { translateWithParameters } from '../../../../../helpers/l10n';
-import { formatMeasure } from '../../../../../helpers/measures';
+import { formatMeasure } from '../../../../../sonar-aligned/helpers/measures';
import { BranchLike } from '../../../../../types/branch-like';
import { MetricType } from '../../../../../types/metrics';
import Tooltip from '../../../components/controls/Tooltip';
import DateFromNow from '../../../components/intl/DateFromNow';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
import { orderLinks } from '../../../helpers/projectLinks';
import { getProjectUrl } from '../../../helpers/urls';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { MyProject, ProjectLink, Status } from '../../../types/types';
import { HelperHintIcon } from 'design-system';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
import HelpTooltip from '../../../sonar-aligned/components/controls/HelpTooltip';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
// Do not display the pending time for values smaller than this threshold (in ms)
const MIN_PENDING_TIME_THRESHOLD = 1000;
import { translateWithParameters } from '../../../helpers/l10n';
import {
areCCTMeasuresComputed as areCCTMeasuresComputedFn,
- formatMeasure,
isDiffMetric,
} from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { isApplication, isProject } from '../../../types/component';
import { MetricKey, MetricType } from '../../../types/metrics';
import { Metric, Status, ComponentMeasure as TypeComponentMeasure } from '../../../types/types';
import { Profile, bulkActivateRules, bulkDeactivateRules } from '../../../api/quality-profiles';
import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { Languages } from '../../../types/languages';
import { MetricType } from '../../../types/metrics';
import { Dict } from '../../../types/types';
import * as React from 'react';
import Tooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { Dict } from '../../../types/types';
import { FacetItemsList } from '../../issues/sidebar/FacetItemsList';
import Tooltip from '../../../components/controls/Tooltip';
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
import { getIssuesUrl } from '../../../helpers/urls';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { Feature } from '../../../types/features';
import { FacetName } from '../../../types/issues';
import { MetricType } from '../../../types/metrics';
import Tooltip from '../../../components/controls/Tooltip';
import Measure from '../../../components/measure/Measure';
import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { isDiffMetric } from '../../../helpers/measures';
import { getMeasureHistoryUrl } from '../../../helpers/urls';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
import { MetricKey, MetricType } from '../../../types/metrics';
translate,
translateWithParameters,
} from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { isDiffMetric } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { getComponentDrilldownUrl } from '../../../helpers/urls';
import HelpTooltip from '../../../sonar-aligned/components/controls/HelpTooltip';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { isProject, isView } from '../../../types/component';
import { MetricKey } from '../../../types/metrics';
import { ColorFilterOption, ColorsLegend } from 'design-system';
import * as React from 'react';
import { translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
export interface ColorRatingsLegendProps {
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric, isPeriodBestValue } from '../../../helpers/measures';
+import { isDiffMetric, isPeriodBestValue } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { MeasurePageView } from '../../../types/measures';
import { MetricType } from '../../../types/metrics';
import * as React from 'react';
import Measure from '../../../components/measure/Measure';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure, getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures';
+import { getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { ComponentMeasureEnhanced, MeasureEnhanced, Metric } from '../../../types/types';
import ColorGradientLegend from '../../../components/charts/ColorGradientLegend';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { isDiffMetric } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricKey, MetricType } from '../../../types/metrics';
import { ComponentMeasureEnhanced, ComponentMeasureIntern, Metric } from '../../../types/types';
import EmptyResult from './EmptyResult';
import React from 'react';
import Measure from '../../../components/measure/Measure';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { isDiffMetric } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { MeasureEnhanced } from '../../../types/types';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
export default function TotalEffort({ effort }: { effort: number }) {
return (
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import { parseDate } from '../../../helpers/dates';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { Component, Dict } from '../../../types/types';
import { Query } from '../utils';
import ListFooter from '../../../components/controls/ListFooter';
import Tooltip from '../../../components/controls/Tooltip';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
import { queriesEqual } from '../../../helpers/query';
import { isDefined } from '../../../helpers/types';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { Dict, Paging, RawQuery } from '../../../types/types';
import { FacetItemsList } from './FacetItemsList';
import { DiscreetLink, Theme, themeColor } from 'design-system';
import * as React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
export interface Props {
import { intersection, isArray, uniq } from 'lodash';
import { getUsers } from '../../api/users';
import { DEFAULT_ISSUES_QUERY } from '../../components/shared/utils';
-import { formatMeasure } from '../../helpers/measures';
import {
cleanQuery,
parseAsArray,
} from '../../helpers/query';
import { get, save } from '../../helpers/storage';
import { isDefined } from '../../helpers/types';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import {
CleanCodeAttributeCategory,
SoftwareImpactSeverity,
import { TrendDirection, TrendIcon, TrendType, themeColor } from 'design-system';
import React from 'react';
import { FormattedMessage } from 'react-intl';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { AnalysisMeasuresVariations } from '../../../types/project-activity';
import { getCurrentPage } from '../../../app/components/nav/component/utils';
import ComponentReportActions from '../../../components/controls/ComponentReportActions';
import HomePageSelect from '../../../components/controls/HomePageSelect';
-import { findMeasure, formatMeasure } from '../../../helpers/measures';
+import { findMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { Branch } from '../../../types/branch-like';
import { MetricKey, MetricType } from '../../../types/metrics';
import { Component, MeasureEnhanced } from '../../../types/types';
import { useIntl } from 'react-intl';
import { getLeakValue } from '../../../components/measure/utils';
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
-import { findMeasure, formatMeasure, formatRating } from '../../../helpers/measures';
+import { findMeasure, formatRating } from '../../../helpers/measures';
import {
CodeScope,
getComponentIssuesUrl,
getComponentSecurityHotspotsUrl,
} from '../../../helpers/urls';
import { getBranchLikeQuery } from '../../../sonar-aligned/helpers/branch-like';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { Branch } from '../../../types/branch-like';
import { isApplication } from '../../../types/component';
import { IssueStatus } from '../../../types/issues';
} from 'design-system';
import * as React from 'react';
import { useIntl } from 'react-intl';
-import { findMeasure, formatMeasure, formatRating } from '../../../helpers/measures';
+import { findMeasure, formatRating } from '../../../helpers/measures';
import {
CodeScope,
getComponentIssuesUrl,
getComponentSecurityHotspotsUrl,
} from '../../../helpers/urls';
import { getBranchLikeQuery } from '../../../sonar-aligned/helpers/branch-like';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { Branch } from '../../../types/branch-like';
import { SoftwareQuality } from '../../../types/clean-code-taxonomy';
import { isApplication } from '../../../types/component';
propsToIssueParams,
} from '../../../components/shared/utils';
import { translate } from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures';
+import { isDiffMetric, localizeMetric } from '../../../helpers/measures';
import { getOperatorLabel } from '../../../helpers/qualityGates';
import {
getComponentDrilldownUrl,
getComponentSecurityHotspotsUrl,
} from '../../../helpers/urls';
import { getBranchLikeQuery } from '../../../sonar-aligned/helpers/branch-like';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { IssueType } from '../../../types/issues';
import { MetricKey, MetricType } from '../../../types/metrics';
if (metric.type !== MetricType.Rating) {
const actual = (condition.period ? measure.period?.value : measure.value) as string;
const formattedValue = formatMeasure(actual, metric.type, {
- decimal: 2,
+ decimals: 1,
omitExtraDecimalZeros: metric.type === MetricType.Percent,
});
return `${formattedValue} ${subText}`;
import * as React from 'react';
import { propsToIssueParams } from '../../../components/shared/utils';
import { translate } from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures';
+import { isDiffMetric, localizeMetric } from '../../../helpers/measures';
import { getComponentIssuesUrl } from '../../../helpers/urls';
import { getBranchLikeQuery } from '../../../sonar-aligned/helpers/branch-like';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { MetricKey, MetricType } from '../../../types/metrics';
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
import * as React from 'react';
import { useIntl } from 'react-intl';
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
-import { formatMeasure } from '../../../helpers/measures';
import { getComponentIssuesUrl } from '../../../helpers/urls';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { Branch } from '../../../types/branch-like';
import { SoftwareImpactSeverity, SoftwareQuality } from '../../../types/clean-code-taxonomy';
import { MetricType } from '../../../types/metrics';
SOFTWARE_QUALITIES_METRIC_KEYS_MAP,
getIssueTypeBySoftwareQuality,
} from '../../../helpers/issues';
-import { formatMeasure } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { getComponentIssuesUrl } from '../../../helpers/urls';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { Branch } from '../../../types/branch-like';
import {
SoftwareImpactMeasureData,
import * as React from 'react';
import { useIntl } from 'react-intl';
import { To } from 'react-router-dom';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricKey, MetricType } from '../../../types/metrics';
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
import { Status, getConditionRequiredLabel } from '../utils';
import { To } from 'react-router-dom';
import { duplicationRatingConverter, getLeakValue } from '../../../components/measure/utils';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures';
+import { findMeasure, localizeMetric } from '../../../helpers/measures';
import { getComponentDrilldownUrl } from '../../../helpers/urls';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { MetricKey, MetricType } from '../../../types/metrics';
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
propsToIssueParams,
} from '../../../components/shared/utils';
import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
-import { formatMeasure, getShortType, isDiffMetric } from '../../../helpers/measures';
+import { getShortType, isDiffMetric } from '../../../helpers/measures';
import {
getComponentDrilldownUrl,
getComponentIssuesUrl,
getComponentSecurityHotspotsUrl,
} from '../../../helpers/urls';
import { getBranchLikeQuery } from '../../../sonar-aligned/helpers/branch-like';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { IssueType } from '../../../types/issues';
import { MetricType } from '../../../types/metrics';
import { useIntl } from 'react-intl';
import { getLeakValue } from '../../../components/measure/utils';
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
-import { findMeasure, formatMeasure } from '../../../helpers/measures';
+import { findMeasure } from '../../../helpers/measures';
import { getComponentIssuesUrl } from '../../../helpers/urls';
import { getBranchLikeQuery } from '../../../sonar-aligned/helpers/branch-like';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { PullRequest } from '../../../types/branch-like';
import { MetricKey, MetricType } from '../../../types/metrics';
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
import { useIntl } from 'react-intl';
import CurrentBranchLikeMergeInformation from '../../../app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation';
import { getLeakValue } from '../../../components/measure/utils';
-import { findMeasure, formatMeasure } from '../../../helpers/measures';
+import { findMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { PullRequest } from '../../../types/branch-like';
import { MetricKey, MetricType } from '../../../types/metrics';
import { MeasureEnhanced } from '../../../types/types';
import { IntlShape } from 'react-intl';
import { ISSUETYPE_METRIC_KEYS_MAP } from '../../helpers/issues';
import { translate } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
import { parseAsString } from '../../helpers/query';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { SoftwareQuality } from '../../types/clean-code-taxonomy';
import { IssueType } from '../../types/issues';
import { MetricKey, MetricType } from '../../types/metrics';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
export interface LifetimeInformationRendererProps {
branchAndPullRequestLifeTimeInDays?: string;
import { DrilldownLink, Note, SizeIndicator, SubHeading } from 'design-system';
import * as React from 'react';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import { formatMeasure, localizeMetric } from '../../../../helpers/measures';
+import { localizeMetric } from '../../../../helpers/measures';
import { getComponentDrilldownUrl } from '../../../../helpers/urls';
+import { formatMeasure } from '../../../../sonar-aligned/helpers/measures';
import { ComponentQualifier } from '../../../../types/component';
import { MetricKey, MetricType } from '../../../../types/metrics';
import { Component, Measure } from '../../../../types/types';
import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
import Measure from '../../../../components/measure/Measure';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import { formatMeasure } from '../../../../helpers/measures';
import { isDefined } from '../../../../helpers/types';
import { getProjectUrl } from '../../../../helpers/urls';
+import { formatMeasure } from '../../../../sonar-aligned/helpers/measures';
import { ComponentQualifier } from '../../../../types/component';
import { MetricKey, MetricType } from '../../../../types/metrics';
import { Status } from '../../../../types/types';
import { MetricsRatingBadge, RatingEnum } from 'design-system';
import * as React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { RawQuery } from '../../../types/types';
import { Facet } from '../types';
import { MetricsRatingBadge, RatingEnum } from 'design-system';
import * as React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { Dict, RawQuery } from '../../../types/types';
import { Facet } from '../types';
import { FormattedMessage } from 'react-intl';
import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
import DocHelpTooltip from '../../../sonar-aligned/components/controls/DocHelpTooltip';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricKey } from '../../../types/metrics';
import { Condition, Dict, Metric } from '../../../types/types';
import { getCaycConditionMetadata, getLocalizedMetricNameNoDiffMetric } from '../utils';
import classNames from 'classnames';
import { themeColor } from 'design-system';
import * as React from 'react';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { Condition, Metric } from '../../../types/types';
import { getCorrectCaycCondition } from '../utils';
import ConditionValueDescription from './ConditionValueDescription';
*/
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricKey } from '../../../types/metrics';
import { Condition, Metric } from '../../../types/types';
import { GreenColorText } from './ConditionValue';
import { ContentCell, Link, Note, NumericalCell, TableRow } from 'design-system';
import * as React from 'react';
import { translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { getRulesUrl } from '../../../helpers/urls';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { RulesFacetName } from '../../../types/rules';
import { listRules } from '../../../api/rules';
import { toShortISO8601String } from '../../../helpers/dates';
import { translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
import { getRulesUrl } from '../../../helpers/urls';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { MetricType } from '../../../types/metrics';
import { Rule, RuleActivation } from '../../../types/types';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
import { save } from '../../../helpers/storage';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { HotspotStatusOption } from '../../../types/security-hotspots';
import { SHOW_STATUS_DIALOG_STORAGE_KEY } from '../constants';
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { each, memoize, omit, omitBy, pickBy, sortBy } from 'lodash';
-import { formatMeasure } from '../../helpers/measures';
import { cleanQuery, parseAsArray, parseAsString, serializeStringArray } from '../../helpers/query';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import {
RawQuery,
SysInfoAppNode,
import { CodeSnippet, Spinner } from 'design-system';
import * as React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { WebhookDelivery } from '../../../types/webhook';
import { formatPayload } from '../utils';
SOFTWARE_QUALITIES_METRIC_KEYS_MAP,
getIssueTypeBySoftwareQuality,
} from '../../helpers/issues';
-import {
- areCCTMeasuresComputed as areCCTMeasuresComputedFn,
- formatMeasure,
-} from '../../helpers/measures';
+import { areCCTMeasuresComputed as areCCTMeasuresComputedFn } from '../../helpers/measures';
import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
import { omitNil } from '../../helpers/request';
import { getBaseUrl } from '../../helpers/system';
getComponentSecurityHotspotsUrl,
} from '../../helpers/urls';
import { getBranchLikeQuery } from '../../sonar-aligned/helpers/branch-like';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import type { BranchLike } from '../../types/branch-like';
import { ComponentQualifier } from '../../types/component';
import { IssueType } from '../../types/issues';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate, translateWithParameters } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { ParsedAnalysis, Serie } from '../../types/project-activity';
import DateFormatter from '../intl/DateFormatter';
import TimeFormatter from '../intl/TimeFormatter';
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
import { AdvancedTimeline } from '../../components/charts/AdvancedTimeline';
import { translate } from '../../helpers/l10n';
-import { formatMeasure, getShortType } from '../../helpers/measures';
+import { getShortType } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { MeasureHistory, ParsedAnalysis, Serie } from '../../types/project-activity';
import ModalButton from '../controls/ModalButton';
import DataTableModal from './DataTableModal';
import { TableSeparator } from 'design-system';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { MetricKey, MetricType } from '../../types/metrics';
import { MeasureHistory } from '../../types/project-activity';
import { TableSeparator } from 'design-system';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { MetricKey, MetricType } from '../../types/metrics';
import { MeasureHistory } from '../../types/project-activity';
import styled from '@emotion/styled';
import { ScaleLinear, ScaleOrdinal } from 'd3-scale';
import * as React from 'react';
-import { formatMeasure } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
interface Props {
colorNA?: string;
import * as React from 'react';
import withLanguagesContext from '../../app/components/languages/withLanguagesContext';
import { translate } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { Languages } from '../../types/languages';
import { MetricType } from '../../types/metrics';
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { formatMeasure } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { MetricType } from '../../types/metrics';
export interface PageCounterProps {
import { ButtonSecondary, Spinner, themeColor } from 'design-system';
import * as React from 'react';
import { translate, translateWithParameters } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { MetricType } from '../../types/metrics';
export interface ListFooterProps {
*/
import * as React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure } from '../../../sonar-aligned/helpers/measures';
import { IssueChangelogDiff as TypeIssueChangelogDiff } from '../../../types/types';
export interface IssueChangelogDiffProps {
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import IssueChangelogDiff, { IssueChangelogDiffProps } from '../IssueChangelogDiff';
-jest.mock('../../../../helpers/measures', () => ({
+jest.mock('../../../../sonar-aligned/helpers/measures', () => ({
formatMeasure: jest
.fn()
.mockImplementation((value: string, type: string) => `formatted.${value}.as.${type}`),
import * as React from 'react';
import Tooltip from '../../components/controls/Tooltip';
import { translate, translateWithParameters } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { MetricType } from '../../types/metrics';
import { Status } from '../../types/types';
import RatingTooltipContent from './RatingTooltipContent';
interface Props {
className?: string;
- decimals?: number | null;
+ decimals?: number;
metricKey: string;
metricType: string;
small?: boolean;
RatingEnum,
} from 'design-system';
import * as React from 'react';
-import { formatMeasure } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { MetricKey, MetricType } from '../../types/metrics';
import Measure from './Measure';
import { duplicationRatingConverter } from './utils';
interface Props {
className?: string;
- decimals?: number | null;
+ decimals?: number;
metricKey: string;
metricType: string;
small?: boolean;
import * as React from 'react';
import withAppStateContext from '../../app/components/app-state/withAppStateContext';
import { translate, translateWithParameters } from '../../helpers/l10n';
-import { formatMeasure, isDiffMetric } from '../../helpers/measures';
+import { isDiffMetric } from '../../helpers/measures';
import {
DIFF_METRIC_PREFIX_LENGTH,
GRID_INDEX_OFFSET,
PERCENT_MULTIPLIER,
getMaintainabilityGrid,
} from '../../helpers/ratings';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { AppState } from '../../types/appstate';
import { MetricKey, MetricType } from '../../types/metrics';
import { GlobalSettingKeys } from '../../types/settings';
import { Note, themeColor } from 'design-system';
import React from 'react';
import { translate } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
import { isDefined } from '../../helpers/types';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { MetricType } from '../../types/metrics';
interface Props {
import classNames from 'classnames';
import * as React from 'react';
import { translate, translateWithParameters } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
+import { formatMeasure } from '../../sonar-aligned/helpers/measures';
import { MetricType } from '../../types/metrics';
import './Rating.css';
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { MetricKey, MetricType } from '../../types/metrics';
-import { Dict } from '../../types/types';
+import { MetricKey } from '../../types/metrics';
import { CCT_SOFTWARE_QUALITY_METRICS } from '../constants';
-import { getMessages } from '../l10nBundle';
import {
areCCTMeasuresComputed,
enhanceConditionWithMeasure,
- formatMeasure,
isPeriodBestValue,
} from '../measures';
import { mockQualityGateStatusCondition } from '../mocks/quality-gates';
import { mockMeasure, mockMeasureEnhanced, mockMetric } from '../testMocks';
-jest.unmock('../l10n');
-
-jest.mock('../l10nBundle', () => ({
- getCurrentLocale: jest.fn().mockReturnValue('us'),
- getMessages: jest.fn().mockReturnValue({}),
-}));
-
-const resetMessages = (messages: Dict<string>) =>
- (getMessages as jest.Mock).mockReturnValue(messages);
-
-beforeAll(() => {
- resetMessages({
- 'work_duration.x_days': '{0}d',
- 'work_duration.x_hours': '{0}h',
- 'work_duration.x_minutes': '{0}min',
- 'work_duration.about': '~ {0}',
- 'metric.level.ERROR': 'Error',
- 'metric.level.WARN': 'Warning',
- 'metric.level.OK': 'Ok',
- 'short_number_suffix.g': 'G',
- 'short_number_suffix.k': 'k',
- 'short_number_suffix.m': 'M',
- });
-});
-
-const HOURS_IN_DAY = 8;
-const ONE_MINUTE = 1;
-const ONE_HOUR = ONE_MINUTE * 60;
-const ONE_DAY = HOURS_IN_DAY * ONE_HOUR;
-
describe('enhanceConditionWithMeasure', () => {
it('should correctly map enhance conditions with measure data', () => {
const measures = [
});
});
-describe('#formatMeasure()', () => {
- it('should format INT', () => {
- expect(formatMeasure(0, MetricType.Integer)).toBe('0');
- expect(formatMeasure(1, MetricType.Integer)).toBe('1');
- expect(formatMeasure(-5, MetricType.Integer)).toBe('-5');
- expect(formatMeasure(999, MetricType.Integer)).toBe('999');
- expect(formatMeasure(1000, MetricType.Integer)).toBe('1,000');
- expect(formatMeasure(1529, MetricType.Integer)).toBe('1,529');
- expect(formatMeasure(10000, MetricType.Integer)).toBe('10,000');
- expect(formatMeasure(1234567890, MetricType.Integer)).toBe('1,234,567,890');
- });
-
- it('should format SHORT_INT', () => {
- expect(formatMeasure(0, MetricType.ShortInteger)).toBe('0');
- expect(formatMeasure(1, MetricType.ShortInteger)).toBe('1');
- expect(formatMeasure(999, MetricType.ShortInteger)).toBe('999');
- expect(formatMeasure(1000, MetricType.ShortInteger)).toBe('1k');
- expect(formatMeasure(1529, MetricType.ShortInteger)).toBe('1.5k');
- expect(formatMeasure(10000, MetricType.ShortInteger)).toBe('10k');
- expect(formatMeasure(10678, MetricType.ShortInteger)).toBe('11k');
- expect(formatMeasure(9467890, MetricType.ShortInteger)).toBe('9.5M');
- expect(formatMeasure(994567890, MetricType.ShortInteger)).toBe('995M');
- expect(formatMeasure(999000001, MetricType.ShortInteger)).toBe('999M');
- expect(formatMeasure(999567890, MetricType.ShortInteger)).toBe('1G');
- expect(formatMeasure(1234567890, MetricType.ShortInteger)).toBe('1.2G');
- expect(formatMeasure(11234567890, MetricType.ShortInteger)).toBe('11G');
- });
-
- it('should format FLOAT', () => {
- expect(formatMeasure(0.0, 'FLOAT')).toBe('0.0');
- expect(formatMeasure(1.0, 'FLOAT')).toBe('1.0');
- expect(formatMeasure(1.3, 'FLOAT')).toBe('1.3');
- expect(formatMeasure(1.34, 'FLOAT')).toBe('1.34');
- expect(formatMeasure(50.89, 'FLOAT')).toBe('50.89');
- expect(formatMeasure(100.0, 'FLOAT')).toBe('100.0');
- expect(formatMeasure(123.456, 'FLOAT')).toBe('123.456');
- expect(formatMeasure(123456.7, 'FLOAT')).toBe('123,456.7');
- expect(formatMeasure(1234567890.0, 'FLOAT')).toBe('1,234,567,890.0');
- });
-
- it('should respect FLOAT precision', () => {
- expect(formatMeasure(0.1, 'FLOAT')).toBe('0.1');
- expect(formatMeasure(0.12, 'FLOAT')).toBe('0.12');
- expect(formatMeasure(0.12345, 'FLOAT')).toBe('0.12345');
- expect(formatMeasure(0.123456, 'FLOAT')).toBe('0.12346');
- });
-
- it('should format PERCENT', () => {
- expect(formatMeasure(0.0, MetricType.Percent)).toBe('0.0%');
- expect(formatMeasure(1.0, MetricType.Percent)).toBe('1.0%');
- expect(formatMeasure(1.3, MetricType.Percent)).toBe('1.3%');
- expect(formatMeasure(1.34, MetricType.Percent)).toBe('1.3%');
- expect(formatMeasure(50.89, MetricType.Percent)).toBe('50.9%');
- expect(formatMeasure(100.0, MetricType.Percent)).toBe('100%');
- expect(formatMeasure(50.89, MetricType.Percent, { decimals: 0 })).toBe('50.9%');
- expect(formatMeasure(50.89, MetricType.Percent, { decimals: 1 })).toBe('50.9%');
- expect(formatMeasure(50.89, MetricType.Percent, { decimals: 2 })).toBe('50.89%');
- expect(formatMeasure(50.89, MetricType.Percent, { decimals: 3 })).toBe('50.890%');
- expect(
- formatMeasure(50, MetricType.Percent, { decimals: 0, omitExtraDecimalZeros: true }),
- ).toBe('50.0%');
- expect(
- formatMeasure(50, MetricType.Percent, { decimals: 1, omitExtraDecimalZeros: true }),
- ).toBe('50.0%');
- expect(
- formatMeasure(50, MetricType.Percent, { decimals: 3, omitExtraDecimalZeros: true }),
- ).toBe('50.0%');
- expect(
- formatMeasure(50.89, MetricType.Percent, { decimals: 3, omitExtraDecimalZeros: true }),
- ).toBe('50.89%');
- });
-
- it('should format WORK_DUR', () => {
- expect(formatMeasure(0, 'WORK_DUR')).toBe('0');
- expect(formatMeasure(5 * ONE_DAY, 'WORK_DUR')).toBe('5d');
- expect(formatMeasure(2 * ONE_HOUR, 'WORK_DUR')).toBe('2h');
- expect(formatMeasure(40 * ONE_MINUTE, 'WORK_DUR')).toBe('40min');
- expect(formatMeasure(ONE_MINUTE, 'WORK_DUR')).toBe('1min');
- expect(formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR, 'WORK_DUR')).toBe('5d 2h');
- expect(formatMeasure(2 * ONE_HOUR + ONE_MINUTE, 'WORK_DUR')).toBe('2h 1min');
- expect(formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'WORK_DUR')).toBe('5d 2h');
- expect(formatMeasure(15 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'WORK_DUR')).toBe('15d');
- expect(formatMeasure(-5 * ONE_DAY, 'WORK_DUR')).toBe('-5d');
- expect(formatMeasure(-2 * ONE_HOUR, 'WORK_DUR')).toBe('-2h');
- expect(formatMeasure(-1 * ONE_MINUTE, 'WORK_DUR')).toBe('-1min');
- });
-
- it('should format SHORT_WORK_DUR', () => {
- expect(formatMeasure(0, MetricType.ShortWorkDuration)).toBe('0');
- expect(formatMeasure(5 * ONE_DAY, MetricType.ShortWorkDuration)).toBe('5d');
- expect(formatMeasure(2 * ONE_HOUR, MetricType.ShortWorkDuration)).toBe('2h');
- expect(formatMeasure(ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('1min');
- expect(formatMeasure(40 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('40min');
- expect(formatMeasure(58 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('1h');
- expect(formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR, MetricType.ShortWorkDuration)).toBe('5d');
- expect(formatMeasure(2 * ONE_HOUR + ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('2h');
- expect(formatMeasure(ONE_HOUR + 55 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('2h');
- expect(formatMeasure(3 * ONE_DAY + 6 * ONE_HOUR, MetricType.ShortWorkDuration)).toBe('4d');
- expect(formatMeasure(7 * ONE_HOUR + 59 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('1d');
- expect(
- formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, MetricType.ShortWorkDuration),
- ).toBe('5d');
- expect(
- formatMeasure(15 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, MetricType.ShortWorkDuration),
- ).toBe('15d');
- expect(formatMeasure(7 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('7min');
- expect(formatMeasure(-5 * ONE_DAY, MetricType.ShortWorkDuration)).toBe('-5d');
- expect(formatMeasure(-2 * ONE_HOUR, MetricType.ShortWorkDuration)).toBe('-2h');
- expect(formatMeasure(-1 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('-1min');
-
- expect(formatMeasure(1529 * ONE_DAY, MetricType.ShortWorkDuration)).toBe('1.5kd');
- expect(formatMeasure(1234567 * ONE_DAY, MetricType.ShortWorkDuration)).toBe('1.2Md');
- expect(formatMeasure(12345670 * ONE_DAY + 4 * ONE_HOUR, MetricType.ShortWorkDuration)).toBe(
- '12Md',
- );
- });
-
- it('should format RATING', () => {
- expect(formatMeasure(1, MetricType.Rating)).toBe('A');
- expect(formatMeasure(2, MetricType.Rating)).toBe('B');
- expect(formatMeasure(3, MetricType.Rating)).toBe('C');
- expect(formatMeasure(4, MetricType.Rating)).toBe('D');
- expect(formatMeasure(5, MetricType.Rating)).toBe('E');
- });
-
- it('should format LEVEL', () => {
- expect(formatMeasure('ERROR', MetricType.Level)).toBe('Error');
- expect(formatMeasure('WARN', MetricType.Level)).toBe('Warning');
- expect(formatMeasure('OK', MetricType.Level)).toBe('Ok');
- expect(formatMeasure('UNKNOWN', MetricType.Level)).toBe('UNKNOWN');
- });
-
- it('should format MILLISEC', () => {
- expect(formatMeasure(0, 'MILLISEC')).toBe('0ms');
- expect(formatMeasure(1, 'MILLISEC')).toBe('1ms');
- expect(formatMeasure(173, 'MILLISEC')).toBe('173ms');
- expect(formatMeasure(3649, 'MILLISEC')).toBe('4s');
- expect(formatMeasure(893481, 'MILLISEC')).toBe('15min');
- expect(formatMeasure(17862325, 'MILLISEC')).toBe('298min');
- });
-
- it('should not format unknown type', () => {
- expect(formatMeasure('random value', 'RANDOM_TYPE')).toBe('random value');
- });
-
- it('should return null if value is empty string', () => {
- expect(formatMeasure('', MetricType.Percent)).toBe('');
- });
-
- it('should not fail with undefined', () => {
- expect(formatMeasure(undefined, MetricType.Integer)).toBe('');
- });
-});
-
describe('areCCTMeasuresComputed', () => {
it('returns true when measures include maintainability_,security_,reliability_issues', () => {
expect(
QualityGateStatusCondition,
QualityGateStatusConditionEnhanced,
} from '../types/quality-gates';
-import { Dict, Measure, MeasureEnhanced, Metric } from '../types/types';
+import { Measure, MeasureEnhanced, Metric } from '../types/types';
import {
CCT_SOFTWARE_QUALITY_METRICS,
LEAK_CCT_SOFTWARE_QUALITY_METRICS,
LEAK_OLD_TAXONOMY_METRICS,
- ONE_SECOND,
} from './constants';
-import { translate, translateWithParameters } from './l10n';
-import { getCurrentLocale } from './l10nBundle';
+import { translate } from './l10n';
import { isDefined } from './types';
export const MEASURES_REDIRECTION: Partial<Record<MetricKey, MetricKey>> = {
return value;
};
-const HOURS_IN_DAY = 8;
-
-type Formatter = (value: string | number, options?: Dict<unknown>) => string;
-
-/**
- * Format a measure value for a given type
- * ! For Ratings, use formatRating instead
- */
-export function formatMeasure(
- value: string | number | undefined,
- type: string,
- options?: Dict<unknown>,
-): string {
- const formatter = getFormatter(type);
- // eslint-disable-next-line react-hooks/rules-of-hooks
- return useFormatter(value, formatter, options);
-}
-
type RatingValue = 'A' | 'B' | 'C' | 'D' | 'E';
const RATING_VALUES: RatingValue[] = ['A', 'B', 'C', 'D', 'E'];
export function formatRating(value: string | number | undefined): RatingValue | undefined {
}
return type;
}
-
-function useFormatter(
- value: string | number | undefined,
- formatter: Formatter,
- options?: Dict<unknown>,
-): string {
- return value !== undefined && value !== '' ? formatter(value, options) : '';
-}
-
-function getFormatter(type: string): Formatter {
- const FORMATTERS: Dict<Formatter> = {
- INT: intFormatter,
- SHORT_INT: shortIntFormatter,
- FLOAT: floatFormatter,
- PERCENT: percentFormatter,
- WORK_DUR: durationFormatter,
- SHORT_WORK_DUR: shortDurationFormatter,
- RATING: ratingFormatter,
- LEVEL: levelFormatter,
- MILLISEC: millisecondsFormatter,
- };
- return FORMATTERS[type] || noFormatter;
-}
-
-function numberFormatter(
- value: string | number,
- minimumFractionDigits = 0,
- maximumFractionDigits = minimumFractionDigits,
-) {
- const { format } = new Intl.NumberFormat(getCurrentLocale(), {
- minimumFractionDigits,
- maximumFractionDigits,
- });
- if (typeof value === 'string') {
- return format(parseFloat(value));
- }
- return format(value);
-}
-
-function noFormatter(value: string | number): string | number {
- return value;
-}
-
-function intFormatter(value: string | number): string {
- return numberFormatter(value);
-}
-
-const shortIntFormats = [
- { unit: 1e10, formatUnit: 1e9, fraction: 0, suffix: 'short_number_suffix.g' },
- { unit: 1e9, formatUnit: 1e9, fraction: 1, suffix: 'short_number_suffix.g' },
- { unit: 1e7, formatUnit: 1e6, fraction: 0, suffix: 'short_number_suffix.m' },
- { unit: 1e6, formatUnit: 1e6, fraction: 1, suffix: 'short_number_suffix.m' },
- { unit: 1e4, formatUnit: 1e3, fraction: 0, suffix: 'short_number_suffix.k' },
- { unit: 1e3, formatUnit: 1e3, fraction: 1, suffix: 'short_number_suffix.k' },
-];
-
-function shortIntFormatter(
- value: string | number,
- option?: { roundingFunc?: (x: number) => number },
-): string {
- const roundingFunc = option?.roundingFunc;
- if (typeof value === 'string') {
- value = parseFloat(value);
- }
- for (let i = 0; i < shortIntFormats.length; i++) {
- const { unit, formatUnit, fraction, suffix } = shortIntFormats[i];
- const nextFraction = unit / (shortIntFormats[i + 1] ? shortIntFormats[i + 1].unit / 10 : 1);
- const roundedValue = numberRound(value / unit, nextFraction, roundingFunc);
- if (roundedValue >= 1) {
- return (
- numberFormatter(
- numberRound(value / formatUnit, Math.pow(10, fraction), roundingFunc),
- 0,
- fraction,
- ) + translate(suffix)
- );
- }
- }
-
- return numberFormatter(value);
-}
-
-function numberRound(
- value: number,
- fraction: number = 1000,
- roundingFunc: (x: number) => number = Math.round,
-) {
- return roundingFunc(value * fraction) / fraction;
-}
-
-function floatFormatter(value: string | number): string {
- return numberFormatter(value, 1, 5);
-}
-
-function percentFormatter(
- value: string | number,
- { decimals, omitExtraDecimalZeros }: { decimals?: number; omitExtraDecimalZeros?: boolean } = {},
-): string {
- if (typeof value === 'string') {
- value = parseFloat(value);
- }
- if (value === 100) {
- return '100%';
- } else if (omitExtraDecimalZeros && decimals) {
- // If omitExtraDecimalZeros is true, all trailing decimal 0s will be removed,
- // except for the first decimal.
- // E.g. for decimals=3:
- // - omitExtraDecimalZeros: false, value: 45.450 => 45.450
- // - omitExtraDecimalZeros: true, value: 45.450 => 45.45
- // - omitExtraDecimalZeros: false, value: 85 => 85.000
- // - omitExtraDecimalZeros: true, value: 85 => 85.0
- return `${numberFormatter(value, 1, decimals)}%`;
- }
- return `${numberFormatter(value, decimals || 1)}%`;
-}
-
-function ratingFormatter(value: string | number): string {
- if (typeof value === 'string') {
- value = parseInt(value, 10);
- }
- return String.fromCharCode(97 + value - 1).toUpperCase();
-}
-
-function levelFormatter(value: string | number): string {
- if (typeof value === 'number') {
- value = value.toString();
- }
- const l10nKey = `metric.level.${value}`;
- const result = translate(l10nKey);
-
- // if couldn't translate, return the initial value
- return l10nKey !== result ? result : value;
-}
-
-function millisecondsFormatter(value: string | number): string {
- if (typeof value === 'string') {
- value = parseInt(value, 10);
- }
- const ONE_MINUTE = 60 * ONE_SECOND;
- if (value >= ONE_MINUTE) {
- const minutes = Math.round(value / ONE_MINUTE);
- return `${minutes}min`;
- } else if (value >= ONE_SECOND) {
- const seconds = Math.round(value / ONE_SECOND);
- return `${seconds}s`;
- }
- return `${value}ms`;
-}
-
-/*
- * Debt Formatters
- */
-
-function shouldDisplayDays(days: number): boolean {
- return days > 0;
-}
-
-function shouldDisplayDaysInShortFormat(days: number): boolean {
- return days > 0.9;
-}
-
-function shouldDisplayHours(days: number, hours: number): boolean {
- return hours > 0 && days < 10;
-}
-
-function shouldDisplayHoursInShortFormat(hours: number): boolean {
- return hours > 0.9;
-}
-
-function shouldDisplayMinutes(days: number, hours: number, minutes: number): boolean {
- return minutes > 0 && hours < 10 && days === 0;
-}
-
-function addSpaceIfNeeded(value: string): string {
- return value.length > 0 ? `${value} ` : value;
-}
-
-function formatDuration(isNegative: boolean, days: number, hours: number, minutes: number): string {
- let formatted = '';
- if (shouldDisplayDays(days)) {
- formatted += translateWithParameters('work_duration.x_days', isNegative ? -1 * days : days);
- }
- if (shouldDisplayHours(days, hours)) {
- formatted = addSpaceIfNeeded(formatted);
- formatted += translateWithParameters(
- 'work_duration.x_hours',
- isNegative && formatted.length === 0 ? -1 * hours : hours,
- );
- }
- if (shouldDisplayMinutes(days, hours, minutes)) {
- formatted = addSpaceIfNeeded(formatted);
- formatted += translateWithParameters(
- 'work_duration.x_minutes',
- isNegative && formatted.length === 0 ? -1 * minutes : minutes,
- );
- }
- return formatted;
-}
-
-function formatDurationShort(
- isNegative: boolean,
- days: number,
- hours: number,
- minutes: number,
-): string {
- if (shouldDisplayDaysInShortFormat(days)) {
- const roundedDays = Math.round(days);
- const formattedDays = formatMeasure(
- isNegative ? -1 * roundedDays : roundedDays,
- MetricType.ShortInteger,
- );
- return translateWithParameters('work_duration.x_days', formattedDays);
- }
-
- if (shouldDisplayHoursInShortFormat(hours)) {
- const roundedHours = Math.round(hours);
- const formattedHours = formatMeasure(
- isNegative ? -1 * roundedHours : roundedHours,
- MetricType.ShortInteger,
- );
- return translateWithParameters('work_duration.x_hours', formattedHours);
- }
-
- const formattedMinutes = formatMeasure(
- isNegative ? -1 * minutes : minutes,
- MetricType.ShortInteger,
- );
- return translateWithParameters('work_duration.x_minutes', formattedMinutes);
-}
-
-function durationFormatter(value: string | number): string {
- if (typeof value === 'string') {
- value = parseInt(value, 10);
- }
- if (value === 0) {
- return '0';
- }
- const hoursInDay = HOURS_IN_DAY;
- const isNegative = value < 0;
- const absValue = Math.abs(value);
- const days = Math.floor(absValue / hoursInDay / 60);
- let remainingValue = absValue - days * hoursInDay * 60;
- const hours = Math.floor(remainingValue / 60);
- remainingValue -= hours * 60;
- return formatDuration(isNegative, days, hours, remainingValue);
-}
-
-function shortDurationFormatter(value: string | number): string {
- if (typeof value === 'string') {
- value = parseInt(value, 10);
- }
- if (value === 0) {
- return '0';
- }
- const hoursInDay = HOURS_IN_DAY;
- const isNegative = value < 0;
- const absValue = Math.abs(value);
- const days = absValue / hoursInDay / 60;
- let remainingValue = absValue - Math.floor(days) * hoursInDay * 60;
- const hours = remainingValue / 60;
- remainingValue -= Math.floor(hours) * 60;
- return formatDurationShort(isNegative, days, hours, remainingValue);
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { getMessages } from '../../../helpers/l10nBundle';
+import { MetricType } from '../../../types/metrics';
+import { Dict } from '../../../types/types';
+import { formatMeasure } from '../measures';
+
+const HOURS_IN_DAY = 8;
+const ONE_MINUTE = 1;
+const ONE_HOUR = ONE_MINUTE * 60;
+const ONE_DAY = HOURS_IN_DAY * ONE_HOUR;
+
+jest.unmock('../../../helpers/l10n');
+
+jest.mock('../../../helpers/l10nBundle', () => ({
+ getCurrentLocale: jest.fn().mockReturnValue('us'),
+ getMessages: jest.fn().mockReturnValue({}),
+}));
+
+const resetMessages = (messages: Dict<string>) =>
+ jest.mocked(getMessages).mockReturnValue(messages);
+
+beforeAll(() => {
+ resetMessages({
+ 'work_duration.x_days': '{0}d',
+ 'work_duration.x_hours': '{0}h',
+ 'work_duration.x_minutes': '{0}min',
+ 'work_duration.about': '~ {0}',
+ 'metric.level.ERROR': 'Error',
+ 'metric.level.WARN': 'Warning',
+ 'metric.level.OK': 'Ok',
+ 'short_number_suffix.g': 'G',
+ 'short_number_suffix.k': 'k',
+ 'short_number_suffix.m': 'M',
+ });
+});
+
+describe('#formatMeasure()', () => {
+ it('should format INT', () => {
+ expect(formatMeasure(0, MetricType.Integer)).toBe('0');
+ expect(formatMeasure(1, MetricType.Integer)).toBe('1');
+ expect(formatMeasure(-5, MetricType.Integer)).toBe('-5');
+ expect(formatMeasure(999, MetricType.Integer)).toBe('999');
+ expect(formatMeasure(1000, MetricType.Integer)).toBe('1,000');
+ expect(formatMeasure(1529, MetricType.Integer)).toBe('1,529');
+ expect(formatMeasure(10000, MetricType.Integer)).toBe('10,000');
+ expect(formatMeasure(1234567890, MetricType.Integer)).toBe('1,234,567,890');
+ });
+
+ it('should format SHORT_INT', () => {
+ expect(formatMeasure(0, MetricType.ShortInteger)).toBe('0');
+ expect(formatMeasure(1, MetricType.ShortInteger)).toBe('1');
+ expect(formatMeasure(999, MetricType.ShortInteger)).toBe('999');
+ expect(formatMeasure(1000, MetricType.ShortInteger)).toBe('1k');
+ expect(formatMeasure(1529, MetricType.ShortInteger)).toBe('1.5k');
+ expect(formatMeasure(10000, MetricType.ShortInteger)).toBe('10k');
+ expect(formatMeasure(10678, MetricType.ShortInteger)).toBe('11k');
+ expect(formatMeasure(9467890, MetricType.ShortInteger)).toBe('9.5M');
+ expect(formatMeasure(994567890, MetricType.ShortInteger)).toBe('995M');
+ expect(formatMeasure(999000001, MetricType.ShortInteger)).toBe('999M');
+ expect(formatMeasure(999567890, MetricType.ShortInteger)).toBe('1G');
+ expect(formatMeasure(1234567890, MetricType.ShortInteger)).toBe('1.2G');
+ expect(formatMeasure(11234567890, MetricType.ShortInteger)).toBe('11G');
+ });
+
+ it('should format FLOAT', () => {
+ expect(formatMeasure(0, 'FLOAT')).toBe('0.0');
+ expect(formatMeasure(1, 'FLOAT')).toBe('1.0');
+ expect(formatMeasure(1.3, 'FLOAT')).toBe('1.3');
+ expect(formatMeasure(1.34, 'FLOAT')).toBe('1.34');
+ expect(formatMeasure(50.89, 'FLOAT')).toBe('50.89');
+ expect(formatMeasure(100, 'FLOAT')).toBe('100.0');
+ expect(formatMeasure(123.456, 'FLOAT')).toBe('123.456');
+ expect(formatMeasure(123456.7, 'FLOAT')).toBe('123,456.7');
+ expect(formatMeasure(1234567890, 'FLOAT')).toBe('1,234,567,890.0');
+ });
+
+ it('should respect FLOAT precision', () => {
+ expect(formatMeasure(0.1, 'FLOAT')).toBe('0.1');
+ expect(formatMeasure(0.12, 'FLOAT')).toBe('0.12');
+ expect(formatMeasure(0.12345, 'FLOAT')).toBe('0.12345');
+ expect(formatMeasure(0.123456, 'FLOAT')).toBe('0.12346');
+ });
+
+ it('should format PERCENT', () => {
+ expect(formatMeasure(0, MetricType.Percent)).toBe('0.0%');
+ expect(formatMeasure(1, MetricType.Percent)).toBe('1.0%');
+ expect(formatMeasure(1.3, MetricType.Percent)).toBe('1.3%');
+ expect(formatMeasure(1.34, MetricType.Percent)).toBe('1.3%');
+ expect(formatMeasure(50.89, MetricType.Percent)).toBe('50.9%');
+ expect(formatMeasure(100, MetricType.Percent)).toBe('100%');
+ expect(formatMeasure(50.89, MetricType.Percent, { decimals: 0 })).toBe('50.9%');
+ expect(formatMeasure(50.89, MetricType.Percent, { decimals: 1 })).toBe('50.9%');
+ expect(formatMeasure(50.89, MetricType.Percent, { decimals: 2 })).toBe('50.89%');
+ expect(formatMeasure(50.89, MetricType.Percent, { decimals: 3 })).toBe('50.890%');
+ expect(
+ formatMeasure(50, MetricType.Percent, { decimals: 0, omitExtraDecimalZeros: true }),
+ ).toBe('50.0%');
+ expect(
+ formatMeasure(50, MetricType.Percent, { decimals: 1, omitExtraDecimalZeros: true }),
+ ).toBe('50.0%');
+ expect(
+ formatMeasure(50, MetricType.Percent, { decimals: 3, omitExtraDecimalZeros: true }),
+ ).toBe('50.0%');
+ expect(
+ formatMeasure(50.89, MetricType.Percent, { decimals: 3, omitExtraDecimalZeros: true }),
+ ).toBe('50.89%');
+ });
+
+ it('should format WORK_DUR', () => {
+ expect(formatMeasure(0, 'WORK_DUR')).toBe('0');
+ expect(formatMeasure(5 * ONE_DAY, 'WORK_DUR')).toBe('5d');
+ expect(formatMeasure(2 * ONE_HOUR, 'WORK_DUR')).toBe('2h');
+ expect(formatMeasure(40 * ONE_MINUTE, 'WORK_DUR')).toBe('40min');
+ expect(formatMeasure(ONE_MINUTE, 'WORK_DUR')).toBe('1min');
+ expect(formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR, 'WORK_DUR')).toBe('5d 2h');
+ expect(formatMeasure(2 * ONE_HOUR + ONE_MINUTE, 'WORK_DUR')).toBe('2h 1min');
+ expect(formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'WORK_DUR')).toBe('5d 2h');
+ expect(formatMeasure(15 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'WORK_DUR')).toBe('15d');
+ expect(formatMeasure(-5 * ONE_DAY, 'WORK_DUR')).toBe('-5d');
+ expect(formatMeasure(-2 * ONE_HOUR, 'WORK_DUR')).toBe('-2h');
+ expect(formatMeasure(-1 * ONE_MINUTE, 'WORK_DUR')).toBe('-1min');
+ });
+
+ it('should format SHORT_WORK_DUR', () => {
+ expect(formatMeasure(0, MetricType.ShortWorkDuration)).toBe('0');
+ expect(formatMeasure(5 * ONE_DAY, MetricType.ShortWorkDuration)).toBe('5d');
+ expect(formatMeasure(2 * ONE_HOUR, MetricType.ShortWorkDuration)).toBe('2h');
+ expect(formatMeasure(ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('1min');
+ expect(formatMeasure(40 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('40min');
+ expect(formatMeasure(58 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('1h');
+ expect(formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR, MetricType.ShortWorkDuration)).toBe('5d');
+ expect(formatMeasure(2 * ONE_HOUR + ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('2h');
+ expect(formatMeasure(ONE_HOUR + 55 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('2h');
+ expect(formatMeasure(3 * ONE_DAY + 6 * ONE_HOUR, MetricType.ShortWorkDuration)).toBe('4d');
+ expect(formatMeasure(7 * ONE_HOUR + 59 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('1d');
+ expect(
+ formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, MetricType.ShortWorkDuration),
+ ).toBe('5d');
+ expect(
+ formatMeasure(15 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, MetricType.ShortWorkDuration),
+ ).toBe('15d');
+ expect(formatMeasure(7 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('7min');
+ expect(formatMeasure(-5 * ONE_DAY, MetricType.ShortWorkDuration)).toBe('-5d');
+ expect(formatMeasure(-2 * ONE_HOUR, MetricType.ShortWorkDuration)).toBe('-2h');
+ expect(formatMeasure(-1 * ONE_MINUTE, MetricType.ShortWorkDuration)).toBe('-1min');
+
+ expect(formatMeasure(1529 * ONE_DAY, MetricType.ShortWorkDuration)).toBe('1.5kd');
+ expect(formatMeasure(1234567 * ONE_DAY, MetricType.ShortWorkDuration)).toBe('1.2Md');
+ expect(formatMeasure(12345670 * ONE_DAY + 4 * ONE_HOUR, MetricType.ShortWorkDuration)).toBe(
+ '12Md',
+ );
+ });
+
+ it('should format RATING', () => {
+ expect(formatMeasure(1, MetricType.Rating)).toBe('A');
+ expect(formatMeasure(2, MetricType.Rating)).toBe('B');
+ expect(formatMeasure(3, MetricType.Rating)).toBe('C');
+ expect(formatMeasure(4, MetricType.Rating)).toBe('D');
+ expect(formatMeasure(5, MetricType.Rating)).toBe('E');
+ });
+
+ it('should format LEVEL', () => {
+ expect(formatMeasure('ERROR', MetricType.Level)).toBe('Error');
+ expect(formatMeasure('WARN', MetricType.Level)).toBe('Warning');
+ expect(formatMeasure('OK', MetricType.Level)).toBe('Ok');
+ expect(formatMeasure('UNKNOWN', MetricType.Level)).toBe('UNKNOWN');
+ });
+
+ it('should format MILLISEC', () => {
+ expect(formatMeasure(0, 'MILLISEC')).toBe('0ms');
+ expect(formatMeasure(1, 'MILLISEC')).toBe('1ms');
+ expect(formatMeasure(173, 'MILLISEC')).toBe('173ms');
+ expect(formatMeasure(3649, 'MILLISEC')).toBe('4s');
+ expect(formatMeasure(893481, 'MILLISEC')).toBe('15min');
+ expect(formatMeasure(17862325, 'MILLISEC')).toBe('298min');
+ });
+
+ it('should not format unknown type', () => {
+ expect(formatMeasure('random value', 'RANDOM_TYPE')).toBe('random value');
+ });
+
+ it('should return null if value is empty string', () => {
+ expect(formatMeasure('', MetricType.Percent)).toBe('');
+ });
+
+ it('should not fail with undefined', () => {
+ expect(formatMeasure(undefined, MetricType.Integer)).toBe('');
+ });
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { ONE_SECOND } from '../../helpers/constants';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { getCurrentLocale } from '../../helpers/l10nBundle';
+import { MetricType } from '../../types/metrics';
+
+import { Dict } from '../../types/types';
+
+const HOURS_IN_DAY = 8;
+
+type FormatterOption =
+ | { roundingFunc?: (x: number) => number }
+ | { decimals?: number; omitExtraDecimalZeros?: boolean };
+
+type Formatter = (value: string | number, options?: FormatterOption) => string;
+/**
+ * Format a measure value for a given type
+ * ! For Ratings, use formatRating instead
+ */
+
+export function formatMeasure(
+ value: string | number | undefined,
+ type: string,
+ options?: FormatterOption,
+): string {
+ const formatter = getFormatter(type);
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ return useFormatter(value, formatter, options);
+}
+
+function useFormatter(
+ value: string | number | undefined,
+ formatter: Formatter,
+ options?: FormatterOption,
+): string {
+ return value !== undefined && value !== '' ? formatter(value, options) : '';
+}
+
+function getFormatter(type: string): Formatter {
+ const FORMATTERS: Dict<Formatter> = {
+ INT: intFormatter,
+ SHORT_INT: shortIntFormatter,
+ FLOAT: floatFormatter,
+ PERCENT: percentFormatter,
+ WORK_DUR: durationFormatter,
+ SHORT_WORK_DUR: shortDurationFormatter,
+ RATING: ratingFormatter,
+ LEVEL: levelFormatter,
+ MILLISEC: millisecondsFormatter,
+ };
+ return FORMATTERS[type] || noFormatter;
+}
+
+function noFormatter(value: string | number): string | number {
+ return value;
+}
+
+function numberFormatter(
+ value: string | number,
+ minimumFractionDigits = 0,
+ maximumFractionDigits = minimumFractionDigits,
+) {
+ const { format } = new Intl.NumberFormat(getCurrentLocale(), {
+ minimumFractionDigits,
+ maximumFractionDigits,
+ });
+ if (typeof value === 'string') {
+ return format(parseFloat(value));
+ }
+ return format(value);
+}
+
+function intFormatter(value: string | number): string {
+ return numberFormatter(value);
+}
+
+function shortIntFormatter(
+ value: string | number,
+ option?: { roundingFunc?: (x: number) => number },
+): string {
+ const shortIntFormats = [
+ { unit: 10000000000, formatUnit: 1000000000, fraction: 0, suffix: 'short_number_suffix.g' },
+ { unit: 1000000000, formatUnit: 1000000000, fraction: 1, suffix: 'short_number_suffix.g' },
+ { unit: 10000000, formatUnit: 1000000, fraction: 0, suffix: 'short_number_suffix.m' },
+ { unit: 1000000, formatUnit: 1000000, fraction: 1, suffix: 'short_number_suffix.m' },
+ { unit: 10000, formatUnit: 1000, fraction: 0, suffix: 'short_number_suffix.k' },
+ { unit: 1000, formatUnit: 1000, fraction: 1, suffix: 'short_number_suffix.k' },
+ ];
+
+ const roundingFunc = option?.roundingFunc;
+ if (typeof value === 'string') {
+ value = parseFloat(value);
+ }
+ for (let i = 0; i < shortIntFormats.length; i++) {
+ const { unit, formatUnit, fraction, suffix } = shortIntFormats[i];
+ const nextFraction = unit / (shortIntFormats[i + 1] ? shortIntFormats[i + 1].unit / 10 : 1);
+ const roundedValue = numberRound(value / unit, nextFraction, roundingFunc);
+ if (roundedValue >= 1) {
+ return (
+ numberFormatter(
+ numberRound(value / formatUnit, Math.pow(10, fraction), roundingFunc),
+ 0,
+ fraction,
+ ) + translate(suffix)
+ );
+ }
+ }
+
+ return numberFormatter(value);
+}
+
+function numberRound(
+ value: number,
+ fraction = 1000,
+ roundingFunc: (x: number) => number = Math.round,
+) {
+ return roundingFunc(value * fraction) / fraction;
+}
+
+function floatFormatter(value: string | number): string {
+ return numberFormatter(value, 1, 5);
+}
+
+function percentFormatter(
+ value: string | number,
+ { decimals, omitExtraDecimalZeros }: { decimals?: number; omitExtraDecimalZeros?: boolean } = {},
+): string {
+ if (typeof value === 'string') {
+ value = parseFloat(value);
+ }
+ if (value === 100) {
+ return '100%';
+ } else if (omitExtraDecimalZeros && decimals) {
+ // If omitExtraDecimalZeros is true, all trailing decimal 0s will be removed,
+ // except for the first decimal.
+ // E.g. for decimals=3:
+ // - omitExtraDecimalZeros: false, value: 45.450 => 45.450
+ // - omitExtraDecimalZeros: true, value: 45.450 => 45.45
+ // - omitExtraDecimalZeros: false, value: 85 => 85.000
+ // - omitExtraDecimalZeros: true, value: 85 => 85.0
+ return `${numberFormatter(value, 1, decimals)}%`;
+ }
+ return `${numberFormatter(value, decimals || 1)}%`;
+}
+
+function ratingFormatter(value: string | number): string {
+ if (typeof value === 'string') {
+ value = parseInt(value, 10);
+ }
+ return String.fromCharCode(97 + value - 1).toUpperCase();
+}
+
+function levelFormatter(value: string | number): string {
+ if (typeof value === 'number') {
+ value = value.toString();
+ }
+ const l10nKey = `metric.level.${value}`;
+ const result = translate(l10nKey);
+
+ // if couldn't translate, return the initial value
+ return l10nKey !== result ? result : value;
+}
+
+function millisecondsFormatter(value: string | number): string {
+ if (typeof value === 'string') {
+ value = parseInt(value, 10);
+ }
+ const ONE_MINUTE = 60 * ONE_SECOND;
+ if (value >= ONE_MINUTE) {
+ const minutes = Math.round(value / ONE_MINUTE);
+ return `${minutes}min`;
+ } else if (value >= ONE_SECOND) {
+ const seconds = Math.round(value / ONE_SECOND);
+ return `${seconds}s`;
+ }
+ return `${value}ms`;
+}
+
+function formatDuration(isNegative: boolean, days: number, hours: number, minutes: number): string {
+ let formatted = '';
+ if (shouldDisplayDays(days)) {
+ formatted += translateWithParameters('work_duration.x_days', isNegative ? -1 * days : days);
+ }
+ if (shouldDisplayHours(days, hours)) {
+ formatted = addSpaceIfNeeded(formatted);
+ formatted += translateWithParameters(
+ 'work_duration.x_hours',
+ isNegative && formatted.length === 0 ? -1 * hours : hours,
+ );
+ }
+ if (shouldDisplayMinutes(days, hours, minutes)) {
+ formatted = addSpaceIfNeeded(formatted);
+ formatted += translateWithParameters(
+ 'work_duration.x_minutes',
+ isNegative && formatted.length === 0 ? -1 * minutes : minutes,
+ );
+ }
+ return formatted;
+}
+
+function formatDurationShort(
+ isNegative: boolean,
+ days: number,
+ hours: number,
+ minutes: number,
+): string {
+ if (shouldDisplayDaysInShortFormat(days)) {
+ const roundedDays = Math.round(days);
+ const formattedDays = formatMeasure(
+ isNegative ? -1 * roundedDays : roundedDays,
+ MetricType.ShortInteger,
+ );
+ return translateWithParameters('work_duration.x_days', formattedDays);
+ }
+
+ if (shouldDisplayHoursInShortFormat(hours)) {
+ const roundedHours = Math.round(hours);
+ const formattedHours = formatMeasure(
+ isNegative ? -1 * roundedHours : roundedHours,
+ MetricType.ShortInteger,
+ );
+ return translateWithParameters('work_duration.x_hours', formattedHours);
+ }
+
+ const formattedMinutes = formatMeasure(
+ isNegative ? -1 * minutes : minutes,
+ MetricType.ShortInteger,
+ );
+ return translateWithParameters('work_duration.x_minutes', formattedMinutes);
+}
+
+function durationFormatter(value: string | number): string {
+ if (typeof value === 'string') {
+ value = parseInt(value, 10);
+ }
+ if (value === 0) {
+ return '0';
+ }
+ const hoursInDay = HOURS_IN_DAY;
+ const isNegative = value < 0;
+ const absValue = Math.abs(value);
+ const days = Math.floor(absValue / hoursInDay / 60);
+ let remainingValue = absValue - days * hoursInDay * 60;
+ const hours = Math.floor(remainingValue / 60);
+ remainingValue -= hours * 60;
+ return formatDuration(isNegative, days, hours, remainingValue);
+}
+
+function shortDurationFormatter(value: string | number): string {
+ if (typeof value === 'string') {
+ value = parseInt(value, 10);
+ }
+ if (value === 0) {
+ return '0';
+ }
+ const hoursInDay = HOURS_IN_DAY;
+ const isNegative = value < 0;
+ const absValue = Math.abs(value);
+ const days = absValue / hoursInDay / 60;
+ let remainingValue = absValue - Math.floor(days) * hoursInDay * 60;
+ const hours = remainingValue / 60;
+ remainingValue -= Math.floor(hours) * 60;
+ return formatDurationShort(isNegative, days, hours, remainingValue);
+}
+
+/*
+ * Debt Formatters
+ */
+function shouldDisplayDays(days: number): boolean {
+ return days > 0;
+}
+
+function shouldDisplayDaysInShortFormat(days: number): boolean {
+ return days > 0.9;
+}
+
+function shouldDisplayHours(days: number, hours: number): boolean {
+ return hours > 0 && days < 10;
+}
+
+function shouldDisplayHoursInShortFormat(hours: number): boolean {
+ return hours > 0.9;
+}
+
+function shouldDisplayMinutes(days: number, hours: number, minutes: number): boolean {
+ return minutes > 0 && hours < 10 && days === 0;
+}
+
+function addSpaceIfNeeded(value: string): string {
+ return value.length > 0 ? `${value} ` : value;
+}