Skip to content

Commit

Permalink
Add date and dateTime filtering support to ClientSideDataService and …
Browse files Browse the repository at this point in the history
…improve overall sorting and filtering of dates.
  • Loading branch information
nruffing committed Jan 13, 2024
1 parent ab43450 commit ff1ed9e
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 15 deletions.
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"datagridvue",
"docsearch",
"dragover",
"falsey",
"gapi",
"keyvault",
"navigatable",
Expand Down
195 changes: 195 additions & 0 deletions lib/DataService.Client.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { expect, test, describe } from 'vitest'
import { ClientSideDataService, StubDataService } from './DataService'
import { SortType } from './Sort'
import { DataType } from './DataGridVue'
import { FilterOperator } from './Filter'

interface TestDataItem {
id: number
name: string
date: string
}

const TestDataItemOne = { id: 1, name: 'Test 1', date: '2024-01-13T12:33:00.000Z' }
const TestDataItemTwo = { id: 2, name: 'Test 2', date: '2024-01-12T12:32:00.000Z' }
const TestDataItemThree = { id: 2, name: 'Test 3', date: '2024-01-11T12:31:00.000Z' }
const TestDataItemFour = { id: 4, name: 'Test 4', date: '2024-01-10T12:30:00.000Z' }
const TestDataItemFive = { id: 5, name: 'Test 5', date: '2024-01-10T12:29:00.000Z' }

const TestDataSet = [TestDataItemThree, TestDataItemTwo, TestDataItemFour, TestDataItemFive, TestDataItemOne] as TestDataItem[]

describe('StubDataService', () => {
test('getPageAsync', async () => {
const pageData = await StubDataService.getPageAsync(1, 10, [], undefined)
expect(pageData).toEqual({
totalItems: 0,
dataItems: [],
})
})
})

describe('ClientSideDataService', () => {
test('getPageAsync | pages', async () => {
const dataService = new ClientSideDataService([...TestDataSet])
const pageDataOne = await dataService.getPageAsync(1, 2, [], undefined)
const pageDataTwo = await dataService.getPageAsync(2, 1, [], undefined)
const pageDataThree = await dataService.getPageAsync(2, 3, [], undefined)
expect(pageDataOne).toEqual({
totalItems: TestDataSet.length,
dataItems: [TestDataItemThree, TestDataItemTwo],
})
expect(pageDataTwo).toEqual({
totalItems: TestDataSet.length,
dataItems: [TestDataItemTwo],
})
expect(pageDataThree).toEqual({
totalItems: TestDataSet.length,
dataItems: [TestDataItemFive, TestDataItemOne],
})
})

test('getPageAsync | filters', async () => {
const dataService = new ClientSideDataService([...TestDataSet])

const testCases = [
{
filter: { or: [{ fieldName: 'id', dataType: DataType.number, operator: FilterOperator.equals, value: '3' }], and: undefined },
pageNum: 1,
pageSize: 2,
expectedTotalItems: 0,
expected: [],
},
{
filter: { or: [{ fieldName: 'id', dataType: DataType.number, operator: FilterOperator.equals, value: '4' }], and: undefined },
pageNum: 1,
pageSize: 2,
expectedTotalItems: 1,
expected: [TestDataItemFour],
},
{
filter: { or: [{ fieldName: 'date', dataType: DataType.date, operator: FilterOperator.lessThan, value: '2024-01-11' }], and: undefined },
pageNum: 1,
pageSize: 2,
expectedTotalItems: 2,
expected: [TestDataItemFour, TestDataItemFive],
},
{
filter: { or: [{ fieldName: 'date', dataType: DataType.date, operator: FilterOperator.equals, value: '2024-01-11' }], and: undefined },
pageNum: 1,
pageSize: 2,
expectedTotalItems: 1,
expected: [TestDataItemThree],
},
{
filter: { or: [{ fieldName: 'date', dataType: DataType.dateTime, operator: FilterOperator.equals, value: '2024-01-11' }], and: undefined },
pageNum: 1,
pageSize: 2,
expectedTotalItems: 0,
expected: [],
},
{
filter: {
or: [{ fieldName: 'date', dataType: DataType.dateTime, operator: FilterOperator.equals, value: '2024-01-10T12:30:00.000Z' }],
and: undefined,
},
pageNum: 1,
pageSize: 2,
expectedTotalItems: 1,
expected: [TestDataItemFour],
},
{
filter: {
or: [{ fieldName: 'date', dataType: DataType.dateTime, operator: FilterOperator.equals, value: '2024-01-10T14:30:00.000+02:00' }],
and: undefined,
},
pageNum: 1,
pageSize: 2,
expectedTotalItems: 1,
expected: [TestDataItemFour],
},
]

for (const testCase of testCases) {
const pageData = await dataService.getPageAsync(testCase.pageNum, testCase.pageSize, [], testCase.filter)
expect(pageData, JSON.stringify(testCase)).toEqual({
totalItems: testCase.expectedTotalItems,
dataItems: testCase.expected,
})
}
})

test('getPageAsync | sorts', async () => {
const dataService = new ClientSideDataService([...TestDataSet])

const testCases = [
{
sort: [{ fieldName: 'id', dataType: DataType.number, type: SortType.descending }],
pageNum: 1,
pageSize: 2,
expected: [TestDataItemFive, TestDataItemFour],
},
{
sort: [{ fieldName: 'id', dataType: DataType.number, type: SortType.descending }],
pageNum: 2,
pageSize: 2,
expected: [TestDataItemThree, TestDataItemTwo],
},
{ sort: [{ fieldName: 'id', dataType: DataType.number, type: SortType.descending }], pageNum: 3, pageSize: 2, expected: [TestDataItemOne] },
{ sort: [], pageNum: 3, pageSize: 1, expected: [TestDataItemFour] },
{
sort: [
{ fieldName: 'id', dataType: DataType.number, type: SortType.ascending },
{ fieldName: 'name', dataType: DataType.alphanumeric, type: SortType.descending },
],
pageNum: 1,
pageSize: 3,
expected: [TestDataItemOne, TestDataItemThree, TestDataItemTwo],
},
{
sort: [{ fieldName: 'date', dataType: DataType.date, type: SortType.ascending }],
pageNum: 2,
pageSize: 2,
expected: [TestDataItemThree, TestDataItemTwo],
},
{
sort: [{ fieldName: 'date', dataType: DataType.date, type: SortType.ascending }],
pageNum: 1,
pageSize: 2,
expected: [TestDataItemFour, TestDataItemFive],
},
{
sort: [{ fieldName: 'date', dataType: DataType.dateTime, type: SortType.ascending }],
pageNum: 1,
pageSize: 2,
expected: [TestDataItemFive, TestDataItemFour],
},
]

for (const testCase of testCases) {
const pageData = await dataService.getPageAsync(testCase.pageNum, testCase.pageSize, testCase.sort, undefined)
expect(pageData, JSON.stringify(testCase)).toEqual({
totalItems: TestDataSet.length,
dataItems: testCase.expected,
})
}
})

test('getPageAsync | filters and sorts', async () => {
const dataService = new ClientSideDataService([...TestDataSet])

const filter = { or: [{ fieldName: 'id', dataType: DataType.number, operator: FilterOperator.greaterThan, value: '2' }], and: undefined }
const sort = [
{ fieldName: 'date', dataType: DataType.dateTime, type: SortType.ascending },
{ fieldName: 'name', dataType: DataType.alphanumeric, type: SortType.descending },
]

const pageData = await dataService.getPageAsync(1, 4, sort, filter)

console.log(JSON.stringify(pageData, null, 2))

expect(pageData).toEqual({
totalItems: 2,
dataItems: [TestDataItemFive, TestDataItemFour],
})
})
})
48 changes: 48 additions & 0 deletions lib/DateUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect, test, describe } from 'vitest'
import { justADatePlease, parseDate, getMinDate } from './DateUtils'

describe('justADatePlease', () => {
test('undefined', () => {
expect(justADatePlease(undefined)).toEqual(getMinDate())
})

test('null', () => {
expect(justADatePlease(null)).toEqual(getMinDate())
})

test('date', () => {
const date = new Date(2021, 0, 1, 1, 2, 3, 4)
expect(justADatePlease(date)).toEqual(new Date(Date.UTC(2021, 0, 1, 0, 0, 0, 0)))
})

test('date with offset rollover on day', () => {
const date = new Date(2021, 0, 1, 22, 2, 3, 4)
expect(justADatePlease(date)).toEqual(new Date(Date.UTC(2021, 0, 2, 0, 0, 0, 0)))
})
})

describe('parseDate', () => {
test('undefined', () => {
expect(parseDate(undefined)).toEqual(getMinDate())
})

test('null', () => {
expect(parseDate(null)).toEqual(getMinDate())
})

test('empty', () => {
expect(parseDate('')).toEqual(getMinDate())
})

test('date', () => {
const dateString = '2021-01-01T06:02:03.004-05:00'
const date = parseDate(dateString)
expect(date).toEqual(new Date(Date.UTC(2021, 0, 1, 11, 2, 3, 4)))
})

test('justADate', () => {
const dateString = '2021-01-01T06:02:03.004-05:00'
const date = parseDate(dateString, true)
expect(date).toEqual(new Date(Date.UTC(2021, 0, 1, 0, 0, 0, 0)))
})
})
45 changes: 45 additions & 0 deletions lib/DateUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Return a new JS date with a time of 00:00:00.000+00:00 on 1970-01-01.
* @returns A new JS date with a time of 00:00:00.000+00:00 on 1970-01-01.
*/
export const getMinDate = () => new Date(0)

/**
* Converts a date to a date with a time of 00:00:00.000.
* The year, month, and day will be adjusted to UTC.
* If the date is undefined or null, a minimum date of 1970-01-01T00:00:00.000Z will be returned.
* @param date The date to convert.
* @returns A new date with a time of 00:00:00.000.
*/
export function justADatePlease(date: Date | undefined | null): Date {
if (!date) {
return getMinDate()
}
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0, 0))
}

/**
* Parses a date string into a JS date.
* @param value The date string to parse.
* @param justADate If true, the time will be set to 00:00:00.000 and the year, month, and day will be adjusted to UTC.
* @returns A new date, undefined if the value was undefined, or null if the value was an empty string.
*/
export function parseDate(value: string | undefined | null, justADate: boolean = false): Date {
if (!value) {
return getMinDate()
}

let resolvedValue = value
if (!/(\d\d:\d\d)|Z$/.test(value)) {
/**
* If the value does not contain a time, assume it is a date and append a time of 00:00:00.000Z.
*/
resolvedValue = `${value}T00:00:00.000Z`
}

let result = new Date(resolvedValue) as Date | undefined | null
if (justADate) {
result = justADatePlease(result)
}
return result as Date
}
11 changes: 11 additions & 0 deletions lib/Filter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DataType } from './DataGridVue'
import { parseDate } from './DateUtils'
import Formatter from './Formatter'

/**
Expand Down Expand Up @@ -133,6 +134,7 @@ export function CompileFilterSummary(filter: Filter | undefined): string {

/**
* @ignore
* Filter implementation for client-side filtering (i.e. `ClientSideDataService`).
*/
export const ClientSideFilter = {
filter(filter: Filter, dataItems: any[]): any[] {
Expand Down Expand Up @@ -174,6 +176,10 @@ export const ClientSideFilter = {
return this.evaluateAlphanumericCondition(value as string, condition.operator, condition.value as string)
case DataType.number:
return this.evaluateNumericCondition(value as number, condition.operator, parseFloat(condition.value ?? ''))
case DataType.date:
return this.evaluateDateTimeCondition(parseDate(value, true), condition.operator, parseDate(condition.value, true))
case DataType.dateTime:
return this.evaluateDateTimeCondition(parseDate(value, false), condition.operator, parseDate(condition.value, false))
}

console.warn(`Unknown data type detected while filtering: ${DataType[condition.dataType]}`)
Expand Down Expand Up @@ -219,4 +225,9 @@ export const ClientSideFilter = {
console.warn(`Filter operator ${FilterOperator[operator]} is not supported for columns with the numeric data type`)
return false
},
evaluateDateTimeCondition(value: Date, operator: FilterOperator, conditionValue: Date): boolean {
const valueTime = value.getTime()
const conditionValueTime = conditionValue.getTime()
return this.evaluateNumericCondition(valueTime, operator, conditionValueTime)
},
}
11 changes: 4 additions & 7 deletions lib/Sort.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DataType } from './DataGridVue'
import { parseDate } from './DateUtils'

/**
* @group Sort
Expand Down Expand Up @@ -50,6 +51,7 @@ export interface Sort {

/**
* @ignore
* Sort implementation for client-side sorting (i.e. `ClientSideDataService`).
*/
export const ClientSideSort = {
sort(sort: Sort[], dataItems: any[]) {
Expand Down Expand Up @@ -77,9 +79,9 @@ export const ClientSideSort = {
case DataType.number:
return this.compareNumeric(aValue as number, bValue as number)
case DataType.date:
return this.compareDate(new Date(aValue), new Date(bValue))
return this.compareDateTime(parseDate(aValue, true), parseDate(bValue, true))
case DataType.dateTime:
return this.compareDateTime(new Date(aValue), new Date(bValue))
return this.compareDateTime(parseDate(aValue, false), parseDate(bValue, false))
}

console.warn(`Unknown data type detected while sorting: ${DataType[sort.dataType]}`)
Expand All @@ -97,11 +99,6 @@ export const ClientSideSort = {
compareNumeric(a: number, b: number) {
return a - b
},
compareDate(a: Date, b: Date) {
const aJustDate = new Date(a.getFullYear(), a.getMonth(), a.getDate())
const bJustDate = new Date(b.getFullYear(), b.getMonth(), b.getDate())
return this.compareDateTime(aJustDate, bJustDate)
},
compareDateTime(a: Date, b: Date) {
return a.getTime() - b.getTime()
},
Expand Down
10 changes: 5 additions & 5 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ export default defineConfig({
env: {
VITE_DEBUG_NATIVE_EVENT_VUE: 'true',
},
// browser: {
// enabled: true,
// name: 'chrome',
// provider: 'webdriverio',
// },
browser: {
enabled: true,
name: 'chrome',
provider: 'webdriverio',
},
coverage: {
provider: 'istanbul',
reporter: ['text', 'json', 'html'],
Expand Down
Loading

0 comments on commit ff1ed9e

Please sign in to comment.