fix(grid): normalize numeric values with thousand separators
This commit is contained in:
parent
44784d664c
commit
343ef6fa84
|
|
@ -65,4 +65,117 @@ describe("coerceDataGridCellValue", () => {
|
|||
expect(coerceDataGridCellValue(options)).toBeNull();
|
||||
expect(coerceDataGridCellValue({ ...options, preserveEmptyString: true })).toBe("");
|
||||
});
|
||||
|
||||
it("strips unambiguous thousands separators before numeric coercion", () => {
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "1,234.50",
|
||||
oldValue: 1234.5,
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "float" },
|
||||
}),
|
||||
).toBe(1234.5);
|
||||
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "1,234,567",
|
||||
oldValue: 1234567,
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "int" },
|
||||
}),
|
||||
).toBe(1234567);
|
||||
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "-10,000.00",
|
||||
oldValue: "-10000.00",
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "decimal(18,2)" },
|
||||
}),
|
||||
).toBe("-10000.00");
|
||||
});
|
||||
|
||||
it("preserves exact text for grouped decimals", () => {
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "10,000.00",
|
||||
oldValue: "10000.50",
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "decimal(18,2)" },
|
||||
}),
|
||||
).toBe("10000.00");
|
||||
});
|
||||
|
||||
it("normalizes grouped mantissas with scientific notation", () => {
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "1,234.50e2",
|
||||
oldValue: "0",
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "decimal(18,2)" },
|
||||
}),
|
||||
).toBe("1234.50e2");
|
||||
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "-1,234.5E-2",
|
||||
oldValue: "0",
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "decimal(18,2)" },
|
||||
}),
|
||||
).toBe("-1234.5E-2");
|
||||
});
|
||||
|
||||
it("preserves exact text for grouped integers beyond Number.MAX_SAFE_INTEGER", () => {
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "9,007,199,254,740,993",
|
||||
oldValue: 9007199254740992,
|
||||
databaseType: "mysql",
|
||||
columnInfo: { data_type: "bigint" },
|
||||
}),
|
||||
).toBe("9007199254740993");
|
||||
});
|
||||
|
||||
it("leaves ambiguous single-group values untouched", () => {
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "10,000",
|
||||
oldValue: 10000,
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "int" },
|
||||
}),
|
||||
).toBe("10,000");
|
||||
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "1,000e3",
|
||||
oldValue: 1000000,
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "float" },
|
||||
}),
|
||||
).toBe("1,000e3");
|
||||
});
|
||||
|
||||
it("does not strip commas when the column is not numeric", () => {
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "10,000.00",
|
||||
oldValue: "10,000.00",
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "varchar(255)" },
|
||||
}),
|
||||
).toBe("10,000.00");
|
||||
});
|
||||
|
||||
it("leaves invalid thousand-grouping values untouched", () => {
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "1,23",
|
||||
oldValue: 123,
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "decimal(18,2)" },
|
||||
}),
|
||||
).toBe("1,23");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { GridCellValue } from "@/lib/dataGrid/dataGridSql";
|
||||
import type { DatabaseType, ColumnInfo } from "@/types/database";
|
||||
import { isNumericColumnType } from "@/lib/dataGrid/dataGridColumnType";
|
||||
|
||||
export interface CoerceDataGridCellValueOptions {
|
||||
value: string;
|
||||
|
|
@ -14,12 +15,17 @@ export function coerceDataGridCellValue(options: CoerceDataGridCellValueOptions)
|
|||
if (value === "" && oldValue === null && !options.preserveEmptyString) return null;
|
||||
const postgresArrayValue = coercePostgresArrayValue(options);
|
||||
if (postgresArrayValue !== undefined) return postgresArrayValue;
|
||||
// Excel-pasted values often carry thousands separators (10,000.00) that make
|
||||
// Number() return NaN and the literal fail to convert on the server. Strip
|
||||
// only unambiguous groupings and keep the normalized text for the precision
|
||||
// checks below, so exact values survive as text.
|
||||
const numericText = normalizeGroupedNumberText(value, options.columnInfo);
|
||||
if (typeof oldValue === "number") {
|
||||
const num = Number(value);
|
||||
const num = Number(numericText);
|
||||
if (!Number.isNaN(num)) {
|
||||
if (shouldPreserveNumericText(options, num)) {
|
||||
if (shouldPreserveNumericText(options, num, numericText)) {
|
||||
// Keep precision-sensitive numeric edits as text; JS Number rounds 64-bit integers.
|
||||
const text = value.trim();
|
||||
const text = numericText.trim();
|
||||
if (text === String(oldValue)) return oldValue;
|
||||
return text;
|
||||
}
|
||||
|
|
@ -27,9 +33,9 @@ export function coerceDataGridCellValue(options: CoerceDataGridCellValueOptions)
|
|||
}
|
||||
}
|
||||
if (typeof oldValue === "boolean") {
|
||||
return value === "true" || value === "1";
|
||||
return numericText === "true" || numericText === "1";
|
||||
}
|
||||
return normalizeSmartQuotedJsonInput(value);
|
||||
return normalizeSmartQuotedJsonInput(numericText);
|
||||
}
|
||||
|
||||
export function dataGridCellEditorText(options: { value: GridCellValue | undefined; databaseType: DatabaseType | undefined; columnInfo: Pick<ColumnInfo, "data_type"> | undefined }): string {
|
||||
|
|
@ -92,12 +98,29 @@ function isPostgresArrayColumn(columnInfo: Pick<ColumnInfo, "data_type"> | undef
|
|||
return dataType === "array" || dataType.endsWith("[]") || dataType.startsWith("_");
|
||||
}
|
||||
|
||||
function shouldPreserveNumericText(options: CoerceDataGridCellValueOptions, parsedNumber: number): boolean {
|
||||
const text = options.value.trim();
|
||||
function shouldPreserveNumericText(options: CoerceDataGridCellValueOptions, parsedNumber: number, text: string): boolean {
|
||||
if (!isNumericLiteralText(text)) return false;
|
||||
return shouldPreserveNumericTextForType(options.columnInfo?.data_type, text, parsedNumber);
|
||||
}
|
||||
|
||||
function normalizeGroupedNumberText(value: string, columnInfo: Pick<ColumnInfo, "data_type"> | undefined): string {
|
||||
if (!isNumericColumnType(columnInfo?.data_type)) return value;
|
||||
return stripUnambiguousThousandSeparators(value);
|
||||
}
|
||||
|
||||
function stripUnambiguousThousandSeparators(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
const match = trimmed.match(/^([+-]?\d{1,3}(?:,\d{3})+(?:\.\d+)?)([eE][+-]?\d+)?$/);
|
||||
if (!match) return value;
|
||||
const mantissa = match[1];
|
||||
const exponent = match[2] ?? "";
|
||||
// A lone "1,000" (one comma group, no decimal point) is ambiguous: in
|
||||
// comma-decimal locales it reads as 1.000. Only strip when a decimal point
|
||||
// is present (1,234.56) or there are multiple comma groups (1,234,567).
|
||||
if (/^[+-]?\d{1,3},\d{3}$/.test(mantissa)) return value;
|
||||
return `${mantissa.replace(/,/g, "")}${exponent}`;
|
||||
}
|
||||
|
||||
function postgresArrayElementDataType(dataType: string | undefined): string {
|
||||
const normalized = normalizeDataType(dataType);
|
||||
if (normalized.startsWith("_")) return normalized.slice(1);
|
||||
|
|
|
|||
Loading…
Reference in New Issue