]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12219 Fix formatting of rounded short numbers
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 19 Jun 2019 10:06:37 +0000 (12:06 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 28 Jun 2019 06:45:40 +0000 (08:45 +0200)
server/sonar-web/src/main/js/apps/about/sonarcloud/components/Statistics.tsx
server/sonar-web/src/main/js/apps/about/sonarcloud/components/__tests__/Statistics-test.tsx
server/sonar-web/src/main/js/apps/about/sonarcloud/components/__tests__/__snapshots__/Statistics-test.tsx.snap
server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts
server/sonar-web/src/main/js/helpers/measures.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 8f220c26e357f91899b20c40817e1bd81981cd68..91be927f973a499b49205d1883ba3b8027400a8b 100644 (file)
@@ -21,6 +21,7 @@ import * as React from 'react';
 import { throttle } from 'lodash';
 import CountUp from 'react-countup';
 import { formatMeasure } from '../../../../helpers/measures';
+import { translate } from '../../../../helpers/l10n';
 import { getBaseUrl } from '../../../../helpers/urls';
 import './Statistics.css';
 
@@ -83,9 +84,14 @@ export class StatisticCard extends React.PureComponent<StatisticCardProps, Stati
 
   render() {
     const { statistic } = this.props;
-    const formattedString = formatMeasure(statistic.value, 'SHORT_INT');
-    const value = parseFloat(formattedString.slice(0, -1));
-    const suffix = formattedString.substr(-1);
+    const formattedString = formatMeasure(statistic.value, 'SHORT_INT', {
+      roundingFunc: Math.floor
+    });
+    const value = parseFloat(formattedString);
+    let suffix = formattedString.replace(value.toString(), '');
+    if (suffix === translate('short_number_suffix.g')) {
+      suffix = ' ' + translate('billion');
+    }
     return (
       <div className="sc-stat-card sc-big-spacer-top" ref={node => (this.container = node)}>
         <div className="sc-stat-icon">
index 9387c2f9cec3896a88b02ba0f760240655e18d1b..52626e305520deb354901803552940c1f6d60af2 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { shallow } from 'enzyme';
+import { shallow, ShallowWrapper } from 'enzyme';
 import Statistics, { StatisticCard } from '../Statistics';
 
-const STATISTICS = {
-  icon: 'stat-icon',
-  text: 'my stat',
-  value: 26666
-};
+const STATISTICS = { icon: 'stat-icon', text: 'my stat', value: 26666 };
 
 it('should render', () => {
   expect(shallow(<Statistics statistics={[STATISTICS]} />)).toMatchSnapshot();
 });
 
 it('should render StatisticCard', () => {
-  expect(shallow(<StatisticCard statistic={STATISTICS} />)).toMatchSnapshot();
+  expect(shallowRender()).toMatchSnapshot();
 });
+
+it('should render big numbers correctly', () => {
+  function checkCountUp(wrapper: ShallowWrapper, end: number, suffix: string) {
+    expect(wrapper.find('CountUp').prop('end')).toBe(end);
+    expect(wrapper.find('CountUp').prop('suffix')).toBe(suffix);
+  }
+
+  checkCountUp(
+    shallowRender({ statistic: { ...STATISTICS, value: 999003632 } }),
+    999,
+    'short_number_suffix.m'
+  );
+  checkCountUp(
+    shallowRender({ statistic: { ...STATISTICS, value: 999861538 } }),
+    999,
+    'short_number_suffix.m'
+  );
+  checkCountUp(shallowRender({ statistic: { ...STATISTICS, value: 1100021731 } }), 1.1, ' billion');
+});
+
+function shallowRender(props: Partial<StatisticCard['props']> = {}) {
+  const wrapper = shallow(<StatisticCard statistic={STATISTICS} {...props} />);
+  wrapper.setState({ viewable: true });
+  return wrapper;
+}
index 32a75b432121ce0c965857d31d668dadde31e9fb..0e1f5dc9dc9e386b821d69750901096278e464ed 100644 (file)
@@ -33,6 +33,14 @@ exports[`should render StatisticCard 1`] = `
   <div
     className="sc-stat-content"
   >
+    <CountUp
+      delay={0}
+      duration={4}
+      end={26}
+      suffix="short_number_suffix.k"
+    >
+      <Component />
+    </CountUp>
     <span>
       my stat
     </span>
index 0cc6bc6f10a4f888f60f284528c80a560faf9e87..6cb21fc5aa7edec6edfae7e807904311e4040432 100644 (file)
@@ -60,7 +60,12 @@ describe('#formatMeasure()', () => {
     expect(formatMeasure(1529, 'SHORT_INT')).toBe('1.5k');
     expect(formatMeasure(10000, 'SHORT_INT')).toBe('10k');
     expect(formatMeasure(10678, 'SHORT_INT')).toBe('11k');
-    expect(formatMeasure(1234567890, 'SHORT_INT')).toBe('1G');
+    expect(formatMeasure(9467890, 'SHORT_INT')).toBe('9.5M');
+    expect(formatMeasure(994567890, 'SHORT_INT')).toBe('995M');
+    expect(formatMeasure(999000001, 'SHORT_INT')).toBe('999M');
+    expect(formatMeasure(999567890, 'SHORT_INT')).toBe('1G');
+    expect(formatMeasure(1234567890, 'SHORT_INT')).toBe('1.2G');
+    expect(formatMeasure(11234567890, 'SHORT_INT')).toBe('11G');
   });
 
   it('should format FLOAT', () => {
@@ -130,8 +135,8 @@ describe('#formatMeasure()', () => {
     expect(formatMeasure(-1 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('-1min');
 
     expect(formatMeasure(1529 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('1.5kd');
-    expect(formatMeasure(1234567 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('1Md');
-    expect(formatMeasure(1234567 * ONE_DAY + 2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('1Md');
+    expect(formatMeasure(1234567 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('1.2Md');
+    expect(formatMeasure(12345670 * ONE_DAY + 4 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('12Md');
   });
 
   it('should format RATING', () => {
index f270294757ef198c7816c0bac4b5f62c8bade8ed..a66fbd8999fd392bbd58f505c7d0088196ae8918 100644 (file)
@@ -130,18 +130,44 @@ function intFormatter(value: number): string {
   return numberFormatter(value);
 }
 
-function shortIntFormatter(value: number): string {
-  if (value >= 1e9) {
-    return numberFormatter(value / 1e9) + translate('short_number_suffix.g');
-  } else if (value >= 1e6) {
-    return numberFormatter(value / 1e6) + translate('short_number_suffix.m');
-  } else if (value >= 1e4) {
-    return numberFormatter(value / 1e3) + translate('short_number_suffix.k');
-  } else if (value >= 1e3) {
-    return numberFormatter(value / 1e3, 0, 1) + translate('short_number_suffix.k');
-  } else {
-    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: number,
+  option?: { roundingFunc?: (x: number) => number }
+): string {
+  const roundingFunc = (option && option.roundingFunc) || undefined;
+  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: number): string {
index ea4344eb9bb35fcbe4b7cc8e68c54322dc7cd74a..065d5e7e04b36424bfd285dad6927a36c700ac28 100644 (file)
@@ -18,6 +18,7 @@ any=Any
 ascending=Ascending
 assignee=Assignee
 author=Author
+billion=Billion
 bitbucket=Bitbucket
 back=Back
 backup=Backup