Table: Fixes sorting for number fields (#34722)

* Table: Fixes sorting for number fields

* Refactor: changes after PR comments

* Refactor: found a bug
This commit is contained in:
Hugo Häggmark 2021-05-31 06:12:56 +02:00 committed by GitHub
parent 13768da417
commit 37ab5ec7f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 81 additions and 2 deletions

View File

@ -14,7 +14,7 @@ import {
useTable, useTable,
} from 'react-table'; } from 'react-table';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import { getColumns, sortCaseInsensitive } from './utils'; import { getColumns, sortCaseInsensitive, sortNumber } from './utils';
import { import {
TableColumnResizeActionCallback, TableColumnResizeActionCallback,
TableFilterActionCallback, TableFilterActionCallback,
@ -153,7 +153,8 @@ export const Table: FC<Props> = memo((props: Props) => {
stateReducer: stateReducer, stateReducer: stateReducer,
initialState: getInitialState(initialSortBy, memoizedColumns), initialState: getInitialState(initialSortBy, memoizedColumns),
sortTypes: { sortTypes: {
'alphanumeric-insensitive': sortCaseInsensitive, number: sortNumber, // should be replace with the builtin number when react-table is upgraded, see https://github.com/tannerlinsley/react-table/pull/3235
'alphanumeric-insensitive': sortCaseInsensitive, // should be replace with the builtin string when react-table is upgraded, see https://github.com/tannerlinsley/react-table/pull/3235
}, },
}), }),
[initialSortBy, memoizedColumns, memoizedData, resizable, stateReducer] [initialSortBy, memoizedColumns, memoizedData, resizable, stateReducer]

View File

@ -6,6 +6,7 @@ import {
getFilteredOptions, getFilteredOptions,
getTextAlign, getTextAlign,
rowToFieldValue, rowToFieldValue,
sortNumber,
sortOptions, sortOptions,
valuesToOptions, valuesToOptions,
} from './utils'; } from './utils';
@ -414,4 +415,61 @@ describe('Table utils', () => {
}); });
}); });
}); });
describe('sortNumber', () => {
it.each`
a | b | expected
${{ values: [] }} | ${{ values: [] }} | ${0}
${{ values: [undefined] }} | ${{ values: [undefined] }} | ${0}
${{ values: [null] }} | ${{ values: [null] }} | ${0}
${{ values: [Number.POSITIVE_INFINITY] }} | ${{ values: [Number.POSITIVE_INFINITY] }} | ${0}
${{ values: [Number.NEGATIVE_INFINITY] }} | ${{ values: [Number.NEGATIVE_INFINITY] }} | ${0}
${{ values: [Number.POSITIVE_INFINITY] }} | ${{ values: [Number.NEGATIVE_INFINITY] }} | ${1}
${{ values: [Number.NEGATIVE_INFINITY] }} | ${{ values: [Number.POSITIVE_INFINITY] }} | ${-1}
${{ values: ['infinIty'] }} | ${{ values: ['infinIty'] }} | ${0}
${{ values: ['infinity'] }} | ${{ values: ['not infinity'] }} | ${0}
${{ values: [1] }} | ${{ values: [1] }} | ${0}
${{ values: [1.5] }} | ${{ values: [1.5] }} | ${0}
${{ values: [2] }} | ${{ values: [1] }} | ${1}
${{ values: [25] }} | ${{ values: [2.5] }} | ${1}
${{ values: [2.5] }} | ${{ values: [1.5] }} | ${1}
${{ values: [1] }} | ${{ values: [2] }} | ${-1}
${{ values: [2.5] }} | ${{ values: [25] }} | ${-1}
${{ values: [1.5] }} | ${{ values: [2.5] }} | ${-1}
${{ values: [1] }} | ${{ values: [] }} | ${1}
${{ values: [1] }} | ${{ values: [undefined] }} | ${1}
${{ values: [1] }} | ${{ values: [null] }} | ${1}
${{ values: [1] }} | ${{ values: [Number.POSITIVE_INFINITY] }} | ${-1}
${{ values: [1] }} | ${{ values: [Number.NEGATIVE_INFINITY] }} | ${1}
${{ values: [1] }} | ${{ values: ['infinIty'] }} | ${1}
${{ values: [-1] }} | ${{ values: ['infinIty'] }} | ${1}
${{ values: [] }} | ${{ values: [1] }} | ${-1}
${{ values: [undefined] }} | ${{ values: [1] }} | ${-1}
${{ values: [null] }} | ${{ values: [1] }} | ${-1}
${{ values: [Number.POSITIVE_INFINITY] }} | ${{ values: [1] }} | ${1}
${{ values: [Number.NEGATIVE_INFINITY] }} | ${{ values: [1] }} | ${-1}
${{ values: ['infinIty'] }} | ${{ values: [1] }} | ${-1}
${{ values: ['infinIty'] }} | ${{ values: [-1] }} | ${-1}
`("when called with a: '$a.toString', b: '$b.toString' then result should be '$expected'", ({ a, b, expected }) => {
expect(sortNumber(a, b, '0')).toEqual(expected);
});
it.skip('should have good performance', () => {
const ITERATIONS = 100000;
const a: any = { values: Array(ITERATIONS) };
const b: any = { values: Array(ITERATIONS) };
for (let i = 0; i < ITERATIONS; i++) {
a.values[i] = Math.random() * Date.now();
b.values[i] = Math.random() * Date.now();
}
const start = performance.now();
for (let i = 0; i < ITERATIONS; i++) {
sortNumber(a, b, i.toString(10));
}
const stop = performance.now();
const diff = stop - start;
expect(diff).toBeLessThanOrEqual(20);
});
});
}); });

View File

@ -60,6 +60,7 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
const selectSortType = (type: FieldType): string => { const selectSortType = (type: FieldType): string => {
switch (type) { switch (type) {
case FieldType.number: case FieldType.number:
return 'number';
case FieldType.time: case FieldType.time:
return 'basic'; return 'basic';
default: default:
@ -208,3 +209,22 @@ export function getFilteredOptions(options: SelectableValue[], filterValues?: Se
export function sortCaseInsensitive(a: Row<any>, b: Row<any>, id: string) { export function sortCaseInsensitive(a: Row<any>, b: Row<any>, id: string) {
return String(a.values[id]).localeCompare(String(b.values[id]), undefined, { sensitivity: 'base' }); return String(a.values[id]).localeCompare(String(b.values[id]), undefined, { sensitivity: 'base' });
} }
// sortNumber needs to have great performance as it is called a lot
export function sortNumber(rowA: Row<any>, rowB: Row<any>, id: string) {
const a = toNumber(rowA.values[id]);
const b = toNumber(rowB.values[id]);
return a === b ? 0 : a > b ? 1 : -1;
}
function toNumber(value: any): number {
if (typeof value === 'number') {
return value;
}
if (value === null || value === undefined || value === '' || isNaN(value)) {
return Number.NEGATIVE_INFINITY;
}
return Number(value);
}