fix(grid): normalize numeric values with thousand separators

This commit is contained in:
dienaso 2026-08-04 13:02:30 +08:00 committed by GitHub
parent 44784d664c
commit 343ef6fa84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 143 additions and 7 deletions

View File

@ -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");
});
});

View File

@ -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);