Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions projects/igniteui-angular/src/lib/grids/grid-base.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7382,17 +7382,81 @@ export abstract class IgxGridBaseDirective implements GridType,
*/
public resolveDataTypes(rec) {
if (typeof rec === 'number') {
// Check if number is a valid timestamp (milliseconds since epoch)
if (this.isValidTimestamp(rec)) {
return GridColumnDataType.Date;
}
return GridColumnDataType.Number;
} else if (typeof rec === 'boolean') {
return GridColumnDataType.Boolean;
} else if (typeof rec === 'object' && rec instanceof Date) {
return GridColumnDataType.Date;
} else if (typeof rec === 'string' && (/\.(gif|jpe?g|tiff?|png|webp|bmp)$/i).test(rec)) {
return GridColumnDataType.Image;
} else if (typeof rec === 'string') {
if ((/\.(gif|jpe?g|tiff?|png|webp|bmp)$/i).test(rec)) {
return GridColumnDataType.Image;
}
// Check if string is a valid date string
if (this.isValidDateString(rec)) {
return GridColumnDataType.Date;
}
}
return GridColumnDataType.String;
}

/**
* @hidden
* Checks if a number is a valid timestamp (milliseconds since epoch)
*/
private isValidTimestamp(value: number): boolean {
// Timestamps should be positive integers
if (!Number.isInteger(value) || value <= 0) {
return false;
}
// Reasonable timestamp range: from 1970 to year 2100
// Min: 0 (Jan 1, 1970)
// Max: 4102444800000 (Jan 1, 2100)
const MIN_TIMESTAMP = 0;
const MAX_TIMESTAMP = 4102444800000;
if (value < MIN_TIMESTAMP || value > MAX_TIMESTAMP) {
return false;
}
// Check if it's in milliseconds range (13 digits) to avoid false positives with regular numbers
// Timestamps from year 2001 onwards have 13 digits: 1000000000000 = September 9, 2001
const MIN_TIMESTAMP_MILLISECONDS = 1000000000000;
if (value < MIN_TIMESTAMP_MILLISECONDS) {
return false;
}
return true;
}

/**
* @hidden
* Checks if a string represents a valid date
*/
private isValidDateString(value: string): boolean {
// Empty strings are not dates
if (!value || value.trim().length === 0) {
return false;
}
// Try to parse the string as a date
const parsedDate = new Date(value);
// Check if the date is valid (not NaN) and the string looks like a date
if (isNaN(parsedDate.getTime())) {
return false;
}
// Additional check: ensure the string contains date-like patterns
// to avoid false positives with strings like "2020" which could be just a year or a number string
const datePatterns = [
/^\d{4}-\d{2}-\d{2}/, // ISO format: YYYY-MM-DD
/^\d{2}\/\d{2}\/\d{4}/, // US format: MM/DD/YYYY
/^\d{4}\/\d{2}\/\d{2}/, // Alternative: YYYY/MM/DD
/^\d{2}-\d{2}-\d{4}/, // Alternative: DD-MM-YYYY or MM-DD-YYYY
/^\d{1,2}\/\d{1,2}\/\d{2,4}/, // Flexible: M/D/YY or M/D/YYYY
/^\w{3}\s+\d{1,2},?\s+\d{4}/, // Text month: Jan 1, 2020 or Jan 1 2020
];
return datePatterns.some(pattern => pattern.test(value.trim()));
}

/**
* @hidden
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2402,6 +2402,7 @@ export class IgxPivotGridComponent extends IgxGridBaseDirective implements OnIni
const rowDimensions: IPivotDimension[] = [];
const values: IPivotValue[] = [];
let isFirstDate = true;

fields.forEach((field) => {
const dataType = this.resolveDataTypes(data[0][field]);
switch (dataType) {
Expand Down Expand Up @@ -2445,6 +2446,7 @@ export class IgxPivotGridComponent extends IgxGridBaseDirective implements OnIni
}
}
});

const config: IPivotConfiguration = {
columns: columnDimensions,
rows: rowDimensions,
Expand Down
143 changes: 143 additions & 0 deletions projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2083,6 +2083,149 @@ describe('IgxPivotGrid #pivotGrid', () => {
expect(pivotGrid.values.map(x => x.enabled)).toEqual([true, true]);
});

it('should auto-generate pivot config and detect date fields from timestamp values', () => {
const pivotGrid = fixture.componentInstance.pivotGrid;
pivotGrid.pivotConfiguration = undefined;
pivotGrid.autoGenerateConfig = true;

// Test with timestamp (milliseconds since epoch)
const TEST_TIMESTAMP = 1577829600000; // 2020-01-01 00:00:00 UTC
pivotGrid.data = [{
ProductCategory: 'Clothing',
UnitPrice: 12.81,
SellerName: 'Stanley',
Country: 'Bulgaria',
Date: TEST_TIMESTAMP,
UnitsSold: 282
}];
fixture.detectChanges();

// Verify date field is detected and config is generated
expect(pivotGrid.allDimensions.length).toEqual(4);
expect(pivotGrid.pivotConfiguration.rows.map(x => x.memberName)).toEqual(['AllPeriods']);
expect(pivotGrid.pivotConfiguration.rows.map(x => x.enabled)).toEqual([true]);

// Verify the timestamp is still in its original format (not pre-parsed)
// IgxPivotDateDimension will handle parsing on-demand
expect(pivotGrid.data[0].Date).toBe(TEST_TIMESTAMP);

// values should be UnitPrice and UnitsSold (not Date)
expect(pivotGrid.values.length).toEqual(2);
expect(pivotGrid.values.map(x => x.member)).toEqual(['UnitPrice', 'UnitsSold']);
});

it('should auto-generate pivot config and detect date fields from ISO date strings', () => {
const pivotGrid = fixture.componentInstance.pivotGrid;
pivotGrid.pivotConfiguration = undefined;
pivotGrid.autoGenerateConfig = true;

// Test with ISO date string
const TEST_DATE_STRING = '2008-06-20';
pivotGrid.data = [{
ProductCategory: 'Clothing',
UnitPrice: 12.81,
SellerName: 'Stanley',
Country: 'Bulgaria',
Date: TEST_DATE_STRING,
UnitsSold: 282
}];
fixture.detectChanges();

// Verify date field is detected and config is generated
expect(pivotGrid.allDimensions.length).toEqual(4);
expect(pivotGrid.pivotConfiguration.rows.map(x => x.memberName)).toEqual(['AllPeriods']);
expect(pivotGrid.pivotConfiguration.rows.map(x => x.enabled)).toEqual([true]);

// Verify the date string is still in its original format (not pre-parsed)
// IgxPivotDateDimension will handle parsing on-demand
expect(pivotGrid.data[0].Date).toBe(TEST_DATE_STRING);
});

it('should auto-generate pivot config and detect date fields from US format date strings', () => {
const pivotGrid = fixture.componentInstance.pivotGrid;
pivotGrid.pivotConfiguration = undefined;
pivotGrid.autoGenerateConfig = true;

// Test with US format date string (MM/DD/YYYY)
const TEST_US_DATE_STRING = '10/24/2025';
pivotGrid.data = [{
ProductCategory: 'Clothing',
UnitPrice: 12.81,
SellerName: 'Stanley',
Country: 'Bulgaria',
Date: TEST_US_DATE_STRING,
UnitsSold: 282
}];
fixture.detectChanges();

// Verify date field is detected and config is generated
expect(pivotGrid.allDimensions.length).toEqual(4);
expect(pivotGrid.pivotConfiguration.rows.map(x => x.memberName)).toEqual(['AllPeriods']);
expect(pivotGrid.pivotConfiguration.rows.map(x => x.enabled)).toEqual([true]);

// Verify the date string is still in its original format (not pre-parsed)
// IgxPivotDateDimension will handle parsing on-demand
expect(pivotGrid.data[0].Date).toBe(TEST_US_DATE_STRING);
});

it('should handle multiple date fields and enable only the first one', () => {
const pivotGrid = fixture.componentInstance.pivotGrid;
pivotGrid.pivotConfiguration = undefined;
pivotGrid.autoGenerateConfig = true;

// Test with multiple date fields
const TEST_START_TIMESTAMP = 1577829600000; // 2020-01-01
const TEST_END_DATE_STRING = '2020-12-31';
pivotGrid.data = [{
ProductCategory: 'Clothing',
StartDate: TEST_START_TIMESTAMP, // timestamp
EndDate: TEST_END_DATE_STRING, // date string
UnitPrice: 12.81,
UnitsSold: 282
}];
fixture.detectChanges();

// Verify both date fields are detected
const rowDimensions = pivotGrid.pivotConfiguration.rows;
expect(rowDimensions.length).toEqual(2);
expect(rowDimensions.map(x => x.memberName)).toEqual(['AllPeriods', 'AllPeriods']);

// Only the first date should be enabled by default
expect(rowDimensions[0].enabled).toBe(true);
expect(rowDimensions[1].enabled).toBe(false);

// Verify date values remain in their original format (not pre-parsed)
// IgxPivotDateDimension will handle parsing on-demand
expect(pivotGrid.data[0].StartDate).toBe(TEST_START_TIMESTAMP);
expect(pivotGrid.data[0].EndDate).toBe(TEST_END_DATE_STRING);
});

it('should not treat regular numbers as timestamps', () => {
const pivotGrid = fixture.componentInstance.pivotGrid;
pivotGrid.pivotConfiguration = undefined;
pivotGrid.autoGenerateConfig = true;

// Test with regular small numbers that should not be timestamps
pivotGrid.data = [{
ProductCategory: 'Clothing',
Quantity: 100, // Regular number, not a timestamp
Year: 2020, // Year number, not a timestamp
UnitPrice: 12.81
}];
fixture.detectChanges();

// Verify no date fields were detected (all should be in values or columns)
expect(pivotGrid.pivotConfiguration.rows.length).toEqual(0);

// Numbers should be treated as values
expect(pivotGrid.values.length).toEqual(3);
expect(pivotGrid.values.map(x => x.member).sort()).toEqual(['Quantity', 'Year', 'UnitPrice'].sort());

// Verify values were not converted
expect(pivotGrid.data[0].Quantity).toBe(100);
expect(pivotGrid.data[0].Year).toBe(2020);
});

it('should allow creating IgxPivotDateDimension with no base dimension and setting it later.', () => {
const pivotGrid = fixture.componentInstance.pivotGrid;
const dateDimension = new IgxPivotDateDimension();
Expand Down