Skip to content

Commit

Permalink
perf: improve Date Sorting by optionally pre-parsing date items
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Sep 24, 2024
1 parent 79d1995 commit 6678139
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 17 deletions.
31 changes: 19 additions & 12 deletions examples/vite-demo-vanilla-bundle/src/examples/example02.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import '../material-styles.scss';

const NB_ITEMS = 500;

function randomBetween(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}

export default class Example02 {
private _bindingEventService: BindingEventService;
columnDefinitions: Column[];
Expand Down Expand Up @@ -127,8 +131,8 @@ export default class Example02 {
filterable: true,
filter: { model: Filters.compoundDate },
sortable: true,
type: FieldType.dateIso,
formatter: Formatters.dateIso,
type: FieldType.dateUsShort,
// formatter: Formatters.dateUs,
exportWithFormatter: true
},
{
Expand All @@ -138,9 +142,9 @@ export default class Example02 {
filterable: true,
filter: { model: Filters.compoundDate },
sortable: true,
type: FieldType.dateIso,
outputType: FieldType.dateIso,
formatter: Formatters.dateIso,
type: FieldType.dateUsShort,
// outputType: FieldType.dateUs,
// formatter: Formatters.dateUs,
},
{
id: 'cost', name: 'Cost', field: 'cost',
Expand Down Expand Up @@ -199,7 +203,7 @@ export default class Example02 {
this.gridOptions = {
autoResize: {
bottomPadding: 30,
rightPadding: 30
rightPadding: 50
},
enableTextExport: true,
enableFiltering: true,
Expand Down Expand Up @@ -245,17 +249,20 @@ export default class Example02 {
hideLastUpdateTimestamp: false
},
// forceSyncScrolling: true,
rowTopOffsetRenderType: 'transform' // defaults: 'top'
rowTopOffsetRenderType: 'transform', // defaults: 'top'

// you can improve Date sorting by pre-parsing date items to `Date` object (this avoid reparsing the same dates multiple times)
preParseDateColumns: '__',
};
}

loadData(rowCount: number) {
// mock a dataset
const tmpArray: any[] = [];
for (let i = 0; i < rowCount; i++) {
const randomYear = 2000 + Math.floor(Math.random() * 10);
const randomMonth = Math.floor(Math.random() * 11);
const randomDay = Math.floor((Math.random() * 29));
const randomYearShort = randomBetween(10, 35);
const randomMonth = randomBetween(1, 12);
const randomDay = randomBetween(10, 28);
const randomPercent = Math.round(Math.random() * 100);
const randomCost = (i % 33 === 0) ? null : Math.round(Math.random() * 10000) / 100;

Expand All @@ -266,8 +273,8 @@ export default class Example02 {
duration: Math.round(Math.random() * 100) + '',
percentComplete: randomPercent,
percentCompleteNumber: randomPercent,
start: new Date(randomYear, randomMonth, randomDay),
finish: new Date(randomYear, (randomMonth + 1), randomDay),
start: `${randomMonth}/${randomDay}/${randomYearShort}`,
finish: `${randomMonth + 1}/${randomDay}/${randomYearShort}`,
cost: i % 3 ? randomCost : randomCost !== null ? -randomCost : null,
effortDriven: (i % 5 === 0)
};
Expand Down
9 changes: 9 additions & 0 deletions packages/common/src/interfaces/gridOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,15 @@ export interface GridOption<C extends Column = Column> {
/** Defaults to "auto", extra top-header panel (on top of column header & pre-header) width, it could be a number (pixels) or a string ("100%" or "auto") */
topHeaderPanelWidth?: number | string;

/**
* Pre-parse all dataset items from date string to `Date` object which will improve Sort considerably
* by parsing the dates only once and sort maybe as O(n2) instead of multiple times which is possibly O(log n2).
* We can enable this option via a string prefix, (i.e. if we set the option to "__", it will parse a "start" date string and assign it as a `Date` object to "__start"),
* if however the option is set to `true`, it will overwrite the same property (i.e. parse "start" date string and reassign it as a `Date` object to "start").
* NOTE: When setting this to `true`, it will overwrite the original date and make it a `Date` object, so make sure that your column definition `type` is taking this into consideration.
*/
preParseDateColumns?: boolean | string;

/** Do we want to preserve copied selection on paste? */
preserveCopiedSelectionOnPaste?: boolean;

Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/services/collection.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ export class CollectionService<T = any> {
const sortDirection = sortBy.sortDesc ? SortDirectionNumber.desc : SortDirectionNumber.asc;
const objectProperty = sortBy.property;
const fieldType = sortBy?.fieldType ?? columnDef?.type ?? FieldType.string;
const value1 = (enableTranslateLabel) ? this.translaterService?.translate && this.translaterService.translate((dataRow1[objectProperty as keyof T] || ' ') as string) : dataRow1[objectProperty as keyof T];
const value2 = (enableTranslateLabel) ? this.translaterService?.translate && this.translaterService.translate((dataRow2[objectProperty as keyof T] || ' ') as string) : dataRow2[objectProperty as keyof T];
const value1 = (enableTranslateLabel) ? this.translaterService?.translate?.((dataRow1[objectProperty as keyof T] || ' ') as string) : dataRow1[objectProperty as keyof T];
const value2 = (enableTranslateLabel) ? this.translaterService?.translate?.((dataRow2[objectProperty as keyof T] || ' ') as string) : dataRow2[objectProperty as keyof T];

const sortResult = sortByFieldType(fieldType, value1, value2, sortDirection, columnDef);
if (sortResult !== SortDirectionNumber.neutral) {
Expand Down
10 changes: 8 additions & 2 deletions packages/common/src/services/sort.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
} from '../interfaces/index';
import { EmitterType, FieldType, SortDirection, SortDirectionNumber, type SortDirectionString, } from '../enums/index';
import type { BackendUtilityService } from './backendUtility.service';
import { getDescendantProperty, flattenToParentChildArray } from './utilities';
import { getDescendantProperty, flattenToParentChildArray, isColumnDateType } from './utilities';
import { sortByFieldType } from '../sortComparers/sortUtilities';
import type { SharedService } from './shared.service';
import type { RxJsFacade, Subject } from './rxjsFacade';
Expand Down Expand Up @@ -414,7 +414,9 @@ export class SortService {
// that happens because we just overwrote the entire dataset the DataView.refresh() doesn't detect a row count change so we trigger it manually
this._dataView.onRowCountChanged.notify({ previous: this._dataView.getFilteredItemCount(), current: this._dataView.getLength(), itemCount: this._dataView.getItemCount(), dataView: this._dataView, callingOnRowsChanged: true });
} else {
console.time('sort');
dataView.sort(this.sortComparers.bind(this, sortColumns));
console.timeEnd('sort');
}

grid.invalidate();
Expand Down Expand Up @@ -476,10 +478,14 @@ export class SortService {
sortComparer(sortColumn: ColumnSort, dataRow1: any, dataRow2: any, querySortField?: string): number | undefined {
if (sortColumn?.sortCol) {
const columnDef = sortColumn.sortCol;
const fieldType = columnDef.type || FieldType.string;
const sortDirection = sortColumn.sortAsc ? SortDirectionNumber.asc : SortDirectionNumber.desc;
let queryFieldName1 = querySortField || columnDef.queryFieldSorter || columnDef.queryField || columnDef.field;

if (this._gridOptions.preParseDateColumns && isColumnDateType(fieldType) && sortColumn?.columnId) {
queryFieldName1 = this._gridOptions.preParseDateColumns ? `__${sortColumn.columnId}` : `${sortColumn.columnId}`;
}
let queryFieldName2 = queryFieldName1;
const fieldType = columnDef.type || FieldType.string;

// if user provided a query field name getter callback, we need to get the name on each item independently
if (typeof columnDef.queryFieldNameGetterFn === 'function') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
import {
autoAddEditorFormatterToColumnsWithEditor,
type AutocompleterEditor,
FieldType,
GlobalGridOptions,
GridStateType,
SlickGroupItemMetadataProvider,
Expand All @@ -45,10 +46,13 @@ import {

// utilities
emptyElement,
unsubscribeAll,
isColumnDateType,
mapTempoDateFormatWithFieldType,
SlickEventHandler,
SlickDataView,
SlickGrid,
tryParseDate,
unsubscribeAll,
} from '@slickgrid-universal/common';
import { extend } from '@slickgrid-universal/utils';
import { EventNamingStyle, EventPubSubService } from '@slickgrid-universal/event-pub-sub';
Expand Down Expand Up @@ -169,6 +173,8 @@ export class SlickVanillaGridBundle<TData = any> {
this.slickGrid.autosizeColumns();
this._isAutosizeColsCalled = true;
}

this.preParseDateItemsWhenEnabled(data);
}

get datasetHierarchical(): any[] | undefined {
Expand Down Expand Up @@ -602,6 +608,8 @@ export class SlickVanillaGridBundle<TData = any> {
this.dataView.endUpdate();
}

this.preParseDateItemsWhenEnabled(inputDataset!);

// if you don't want the items that are not visible (due to being filtered out or being on a different page)
// to stay selected, pass 'false' to the second arg
if (this.slickGrid?.getSelectionModel() && this._gridOptions?.dataView?.hasOwnProperty('syncGridSelection')) {
Expand Down Expand Up @@ -1044,6 +1052,33 @@ export class SlickVanillaGridBundle<TData = any> {
});
}

/** Pre-parse date items as `Date` object to improve Date Sort considerably */
protected preParseDateItemsWhenEnabled(items: TData[]): void {
if (this.gridOptions.preParseDateColumns) {
console.time('mutate');
this.columnDefinitions.forEach(col => {
const fieldType = col.type || FieldType.string;
const dateFormat = mapTempoDateFormatWithFieldType(fieldType);
const strictParsing = dateFormat !== undefined;
if (isColumnDateType(fieldType)) {
// preparsing could be a boolean (reassign and overwrite same property)
// OR a prefix string to assign it into a new item property
const queryFieldName = typeof this.gridOptions.preParseDateColumns === 'string'
? `${this.gridOptions.preParseDateColumns}${col.id}`
: `${col.id}`;
items.forEach((item: any) => {
const date = tryParseDate(item[col.id], dateFormat, strictParsing);
if (date) {
item[queryFieldName] = date;
}
});
}
});
console.timeEnd('mutate');
// console.log('data', items);
}
}

/**
* When dataset changes, we need to refresh the entire grid UI & possibly resize it as well
* @param dataset
Expand Down

0 comments on commit 6678139

Please sign in to comment.