mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
440 lines
12 KiB
TypeScript
440 lines
12 KiB
TypeScript
/**
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
import {
|
|
calculateTimeValue,
|
|
calculateContribution,
|
|
calculateAverage,
|
|
calculateCellValue,
|
|
} from './valueCalculations';
|
|
import type { ColumnConfig, Entry } from '../../types';
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('valueCalculations', () => {
|
|
const mockEntries: Entry[] = [
|
|
{ time: '2023-01-03', sales: 300, price: 30 },
|
|
{ time: '2023-01-02', sales: 200, price: 20 },
|
|
{ time: '2023-01-01', sales: 100, price: 10 },
|
|
];
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('calculateTimeValue', () => {
|
|
test('should calculate diff comparison correctly', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'diff',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const result = calculateTimeValue(300, 'sales', mockEntries, column);
|
|
|
|
expect(result.value).toBe(100);
|
|
expect(result.errorMsg).toBeUndefined();
|
|
});
|
|
|
|
test('should skip null values when finding lagged data points', () => {
|
|
const entriesWithNulls: Entry[] = [
|
|
{ time: '2023-01-05', sales: 500, price: 50 },
|
|
{ time: '2023-01-04', sales: null, price: null },
|
|
{ time: '2023-01-03', sales: null, price: null },
|
|
{ time: '2023-01-02', sales: 200, price: 20 },
|
|
{ time: '2023-01-01', sales: 100, price: 10 },
|
|
];
|
|
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'diff',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const result = calculateTimeValue(500, 'sales', entriesWithNulls, column);
|
|
|
|
expect(result.value).toBe(300);
|
|
});
|
|
|
|
test('should maintain consistency between filtered and unfiltered behavior', () => {
|
|
const unfilteredEntries: Entry[] = [
|
|
{ time: '2023-01-05', productA: 500, productB: null },
|
|
{ time: '2023-01-04', productA: null, productB: null },
|
|
{ time: '2023-01-03', productA: null, productB: 300 },
|
|
{ time: '2023-01-02', productA: 200, productB: null },
|
|
{ time: '2023-01-01', productA: 100, productB: 200 },
|
|
];
|
|
|
|
const filteredEntries: Entry[] = [
|
|
{ time: '2023-01-05', productA: 500 },
|
|
{ time: '2023-01-02', productA: 200 },
|
|
{ time: '2023-01-01', productA: 100 },
|
|
];
|
|
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'diff',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const unfilteredResult = calculateTimeValue(
|
|
500,
|
|
'productA',
|
|
unfilteredEntries,
|
|
column,
|
|
);
|
|
const filteredResult = calculateTimeValue(
|
|
500,
|
|
'productA',
|
|
filteredEntries,
|
|
column,
|
|
);
|
|
|
|
expect(unfilteredResult.value).toBe(filteredResult.value);
|
|
expect(unfilteredResult.value).toBe(300);
|
|
});
|
|
|
|
test('should calculate perc comparison correctly', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'perc',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const result = calculateTimeValue(300, 'sales', mockEntries, column);
|
|
|
|
expect(result.value).toBe(1.5); // 300 / 200
|
|
});
|
|
|
|
test('should calculate perc_change comparison correctly', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'perc_change',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const result = calculateTimeValue(300, 'sales', mockEntries, column);
|
|
|
|
expect(result.value).toBe(0.5); // (300 / 200) - 1
|
|
});
|
|
|
|
test('should handle negative time lag', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'diff',
|
|
timeLag: -1,
|
|
};
|
|
|
|
const result = calculateTimeValue(300, 'sales', mockEntries, column);
|
|
|
|
expect(result.value).toBe(200); // 300 - 100 (from end of array)
|
|
});
|
|
|
|
test('should return error for excessive time lag', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
timeLag: 10,
|
|
};
|
|
|
|
const result = calculateTimeValue(300, 'sales', mockEntries, column);
|
|
|
|
expect(result.value).toBeNull();
|
|
expect(result.errorMsg).toContain('too large for the length of data');
|
|
});
|
|
|
|
test('should return null for invalid values', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const result = calculateTimeValue(null, 'sales', mockEntries, column);
|
|
|
|
expect(result.value).toBeNull();
|
|
expect(result.errorMsg).toBeUndefined();
|
|
});
|
|
|
|
test('should return lagged value when no comparison type', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const result = calculateTimeValue(300, 'sales', mockEntries, column);
|
|
|
|
expect(result.value).toBe(200); // lagged value without comparison
|
|
});
|
|
|
|
test('should handle multiple null values with different time lags', () => {
|
|
const entriesWithGaps: Entry[] = [
|
|
{ time: '2023-01-07', sales: 700 },
|
|
{ time: '2023-01-06', sales: null },
|
|
{ time: '2023-01-05', sales: null },
|
|
{ time: '2023-01-04', sales: null },
|
|
{ time: '2023-01-03', sales: 300 },
|
|
{ time: '2023-01-02', sales: 200 },
|
|
{ time: '2023-01-01', sales: 100 },
|
|
];
|
|
|
|
const column1: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'diff',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const column2: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'diff',
|
|
timeLag: 2,
|
|
};
|
|
|
|
const result1 = calculateTimeValue(
|
|
700,
|
|
'sales',
|
|
entriesWithGaps,
|
|
column1,
|
|
);
|
|
const result2 = calculateTimeValue(
|
|
700,
|
|
'sales',
|
|
entriesWithGaps,
|
|
column2,
|
|
);
|
|
|
|
expect(result1.value).toBe(400);
|
|
expect(result2.value).toBe(500);
|
|
});
|
|
|
|
test('should work with perc and perc_change when nulls are present', () => {
|
|
const entriesWithNulls: Entry[] = [
|
|
{ time: '2023-01-04', sales: 400 },
|
|
{ time: '2023-01-03', sales: null },
|
|
{ time: '2023-01-02', sales: null },
|
|
{ time: '2023-01-01', sales: 200 },
|
|
];
|
|
|
|
const percColumn: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'perc',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const percChangeColumn: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'perc_change',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const percResult = calculateTimeValue(
|
|
400,
|
|
'sales',
|
|
entriesWithNulls,
|
|
percColumn,
|
|
);
|
|
const percChangeResult = calculateTimeValue(
|
|
400,
|
|
'sales',
|
|
entriesWithNulls,
|
|
percChangeColumn,
|
|
);
|
|
|
|
expect(percResult.value).toBe(2);
|
|
expect(percChangeResult.value).toBe(1);
|
|
});
|
|
|
|
test('should return null when not enough non-null values for time lag', () => {
|
|
const sparseEntries: Entry[] = [
|
|
{ time: '2023-01-03', sales: 300 },
|
|
{ time: '2023-01-02', sales: null },
|
|
{ time: '2023-01-01', sales: null },
|
|
];
|
|
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'diff',
|
|
timeLag: 2,
|
|
};
|
|
|
|
const result = calculateTimeValue(300, 'sales', sparseEntries, column);
|
|
|
|
expect(result.value).toBeNull();
|
|
});
|
|
});
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('calculateContribution', () => {
|
|
test('should calculate contribution correctly', () => {
|
|
const result = calculateContribution(300, mockEntries);
|
|
|
|
expect(result.value).toBeCloseTo(0.909, 3);
|
|
expect(result.errorMsg).toBeUndefined();
|
|
});
|
|
|
|
test('should return null for null recent value', () => {
|
|
const result = calculateContribution(null, mockEntries);
|
|
expect(result.value).toBeNull();
|
|
});
|
|
|
|
test('should return null for empty entries', () => {
|
|
const result = calculateContribution(300, []);
|
|
expect(result.value).toBeNull();
|
|
});
|
|
|
|
test('should return null when total is zero', () => {
|
|
const zeroEntries: Entry[] = [{ time: '2023-01-01', sales: 0 }];
|
|
|
|
const result = calculateContribution(100, zeroEntries);
|
|
|
|
expect(result.value).toBeNull();
|
|
});
|
|
});
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('calculateAverage', () => {
|
|
test('should calculate average correctly', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'avg',
|
|
timeLag: 2,
|
|
};
|
|
|
|
const result = calculateAverage('sales', mockEntries, column);
|
|
|
|
expect(result.value).toBe(250);
|
|
expect(result.errorMsg).toBeUndefined();
|
|
});
|
|
|
|
test('should handle null/undefined values', () => {
|
|
const entriesWithNulls: Entry[] = [
|
|
{ time: '2023-01-03', sales: 300 },
|
|
{ time: '2023-01-02', sales: null },
|
|
{ time: '2023-01-01', sales: 100 },
|
|
];
|
|
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'avg',
|
|
timeLag: 3,
|
|
};
|
|
|
|
const result = calculateAverage('sales', entriesWithNulls, column);
|
|
|
|
expect(result.value).toBe(200);
|
|
});
|
|
|
|
test('should return null for empty entries', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'avg',
|
|
};
|
|
|
|
const result = calculateAverage('sales', [], column);
|
|
|
|
expect(result.value).toBeNull();
|
|
});
|
|
|
|
test('should return null when all values are null', () => {
|
|
const entriesWithAllNulls: Entry[] = [
|
|
{ time: '2023-01-03', sales: null },
|
|
{ time: '2023-01-02', sales: undefined },
|
|
];
|
|
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'avg',
|
|
timeLag: 2,
|
|
};
|
|
|
|
const result = calculateAverage('sales', entriesWithAllNulls, column);
|
|
|
|
expect(result.value).toBeNull();
|
|
});
|
|
});
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('calculateCellValue', () => {
|
|
test('should route to time calculation', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
comparisonType: 'diff',
|
|
timeLag: 1,
|
|
};
|
|
|
|
const result = calculateCellValue('sales', column, mockEntries);
|
|
|
|
expect(result.value).toBe(100); // 300 - 200
|
|
});
|
|
|
|
test('should route to contribution calculation', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'contrib',
|
|
};
|
|
|
|
const result = calculateCellValue('sales', column, mockEntries);
|
|
|
|
expect(result.value).toBeCloseTo(0.909, 3);
|
|
});
|
|
|
|
test('should route to average calculation', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'avg',
|
|
timeLag: 2,
|
|
};
|
|
|
|
const result = calculateCellValue('sales', column, mockEntries);
|
|
|
|
expect(result.value).toBe(250);
|
|
});
|
|
|
|
test('should return recent value for default case', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
};
|
|
|
|
const result = calculateCellValue('sales', column, mockEntries);
|
|
|
|
expect(result.value).toBe(300); // Recent value
|
|
});
|
|
|
|
test('should return null for empty entries', () => {
|
|
const column: ColumnConfig = {
|
|
key: 'test',
|
|
colType: 'time',
|
|
};
|
|
|
|
const result = calculateCellValue('sales', column, []);
|
|
|
|
expect(result.value).toBeNull();
|
|
});
|
|
});
|
|
});
|