fix(grid): parse ambiguous comma numbers by locale
This commit is contained in:
1 parent
99c3bd0f42
commit
27f62fe6e2
2 files changed
+75
-18
No files matched your search
@@ -250,15 +250,36 @@ describe("coerceDataGridCellValue", () => {
|
||||
).toBe("9007199254740993");
|
||||
});
|
||||
|
||||
it("leaves ambiguous single-group values untouched", () => {
|
||||
it("normalizes ambiguous single-comma values using the runtime number format", () => {
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "10,000",
|
||||
oldValue: 10000,
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "int" },
|
||||
oldValue: "0.0000",
|
||||
databaseType: "mysql",
|
||||
columnInfo: { data_type: "decimal(14,4)" },
|
||||
numberFormat: { groupSeparator: ",", decimalSeparator: "." },
|
||||
}),
|
||||
).toBe("10,000");
|
||||
).toBe("10000");
|
||||
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "114,870",
|
||||
oldValue: "0.0000",
|
||||
databaseType: "mysql",
|
||||
columnInfo: { data_type: "decimal(14,4)" },
|
||||
numberFormat: { groupSeparator: ",", decimalSeparator: "." },
|
||||
}),
|
||||
).toBe("114870");
|
||||
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "114,870",
|
||||
oldValue: "0.0000",
|
||||
databaseType: "mysql",
|
||||
columnInfo: { data_type: "decimal(14,4)" },
|
||||
numberFormat: { groupSeparator: ".", decimalSeparator: "," },
|
||||
}),
|
||||
).toBe("114.870");
|
||||
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
@@ -266,8 +287,21 @@ describe("coerceDataGridCellValue", () => {
|
||||
oldValue: 1000000,
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "float" },
|
||||
numberFormat: { groupSeparator: ".", decimalSeparator: "," },
|
||||
}),
|
||||
).toBe("1,000e3");
|
||||
).toBe(1000);
|
||||
});
|
||||
|
||||
it("leaves ambiguous comma values untouched when the runtime format uses neither comma token", () => {
|
||||
expect(
|
||||
coerceDataGridCellValue({
|
||||
value: "10,000",
|
||||
oldValue: 10000,
|
||||
databaseType: "sqlserver",
|
||||
columnInfo: { data_type: "int" },
|
||||
numberFormat: { groupSeparator: "’", decimalSeparator: "." },
|
||||
}),
|
||||
).toBe("10,000");
|
||||
});
|
||||
|
||||
it("does not strip commas when the column is not numeric", () => {
|
||||
|
||||
@@ -9,11 +9,19 @@ export interface CoerceDataGridCellValueOptions {
|
||||
oldValue: GridCellValue | undefined;
|
||||
databaseType: DatabaseType | undefined;
|
||||
columnInfo: Pick<ColumnInfo, "data_type"> | undefined;
|
||||
numberFormat?: NumericInputNumberFormat;
|
||||
preserveEmptyString?: boolean;
|
||||
/** Treat an empty inline bulk edit as SQL NULL before type coercion. */
|
||||
emptyStringAsNull?: boolean;
|
||||
}
|
||||
|
||||
export interface NumericInputNumberFormat {
|
||||
groupSeparator?: string;
|
||||
decimalSeparator?: string;
|
||||
}
|
||||
|
||||
let cachedRuntimeNumberFormat: NumericInputNumberFormat | undefined;
|
||||
|
||||
export function coerceDataGridCellValue(options: CoerceDataGridCellValueOptions): GridCellValue {
|
||||
const { value, oldValue } = options;
|
||||
if (value === "" && options.emptyStringAsNull) return null;
|
||||
@@ -22,13 +30,13 @@ export function coerceDataGridCellValue(options: CoerceDataGridCellValueOptions)
|
||||
if (blobValue !== undefined) return blobValue;
|
||||
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.
|
||||
// Excel-pasted values often carry separators that make Number() return NaN
|
||||
// and the literal fail to convert on the server. Use the runtime number
|
||||
// format to resolve single-comma values, then keep the normalized text for
|
||||
// the precision checks below so exact values survive as text.
|
||||
const useSampledValueType = normalizeDataType(options.columnInfo?.data_type) === "";
|
||||
const numericInput = isNumericColumnType(options.columnInfo?.data_type) || (useSampledValueType && typeof oldValue === "number");
|
||||
const numericText = normalizeGroupedNumberText(value, options.columnInfo, oldValue);
|
||||
const numericText = normalizeGroupedNumberText(value, options.columnInfo, oldValue, options.numberFormat ?? runtimeNumberFormat());
|
||||
if (isBooleanInputColumn(options) || (useSampledValueType && typeof oldValue === "boolean")) {
|
||||
// MySQL exposes TINYINT(1) as an integer in the grid. Keep its numeric
|
||||
// 0/1 edits numeric while still accepting explicit TRUE/FALSE aliases.
|
||||
@@ -155,25 +163,40 @@ function shouldPreserveNumericText(options: CoerceDataGridCellValueOptions, pars
|
||||
return shouldPreserveNumericTextForType(options.columnInfo?.data_type, text, parsedNumber);
|
||||
}
|
||||
|
||||
function normalizeGroupedNumberText(value: string, columnInfo: Pick<ColumnInfo, "data_type"> | undefined, oldValue: GridCellValue | undefined): string {
|
||||
function normalizeGroupedNumberText(value: string, columnInfo: Pick<ColumnInfo, "data_type"> | undefined, oldValue: GridCellValue | undefined, numberFormat: NumericInputNumberFormat): string {
|
||||
const useSampledNumberType = normalizeDataType(columnInfo?.data_type) === "" && typeof oldValue === "number";
|
||||
if (!isNumericColumnType(columnInfo?.data_type) && !useSampledNumberType) return value;
|
||||
return stripUnambiguousThousandSeparators(value);
|
||||
return normalizeCommaSeparatedNumberText(value, numberFormat);
|
||||
}
|
||||
|
||||
function stripUnambiguousThousandSeparators(value: string): string {
|
||||
function normalizeCommaSeparatedNumberText(value: string, numberFormat: NumericInputNumberFormat): 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;
|
||||
if (/^[+-]?\d{1,3},\d{3}$/.test(mantissa)) {
|
||||
if (numberFormat.groupSeparator === "," && numberFormat.decimalSeparator !== ",") {
|
||||
return `${mantissa.replace(",", "")}${exponent}`;
|
||||
}
|
||||
if (numberFormat.decimalSeparator === ",") {
|
||||
return `${mantissa.replace(",", ".")}${exponent}`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return `${mantissa.replace(/,/g, "")}${exponent}`;
|
||||
}
|
||||
|
||||
function runtimeNumberFormat(): NumericInputNumberFormat {
|
||||
if (cachedRuntimeNumberFormat) return cachedRuntimeNumberFormat;
|
||||
const parts = new Intl.NumberFormat().formatToParts(12345.6);
|
||||
cachedRuntimeNumberFormat = {
|
||||
groupSeparator: parts.find((part) => part.type === "group")?.value,
|
||||
decimalSeparator: parts.find((part) => part.type === "decimal")?.value,
|
||||
};
|
||||
return cachedRuntimeNumberFormat;
|
||||
}
|
||||
|
||||
function postgresArrayElementDataType(dataType: string | undefined): string {
|
||||
const normalized = normalizeDataType(dataType);
|
||||
if (normalized.startsWith("_")) return normalized.slice(1);
|
||||
|
||||
Reference in new issue
Block a user