mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Fix a bug with editing numbers in some locales.
Summary: Adds a new test for formatting and fix several related bugs it uncovered: 1. When editing a number with "," decimal separator, ensure it opens in the editor with "," (rather than ".", the original bug motivating this). 2. When guessing number format, set maxDecimals when it's needed (otherwise, e.g. "$1.234", or "4.5%" weren't guessed as numeric) 3. When guessing number format, ignore whitespace when deciding if guessed format is correct (otherwise percents can't be guessed in locales which add "%" with a non-breaking space before it). Test Plan: Added a test case that exercises all fixed behaviors. Reviewers: paulfitz Reviewed By: paulfitz Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4177
This commit is contained in:
17
app/client/widgets/NumericEditor.ts
Normal file
17
app/client/widgets/NumericEditor.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {FieldOptions} from 'app/client/widgets/NewBaseEditor';
|
||||
import {NTextEditor} from 'app/client/widgets/NTextEditor';
|
||||
|
||||
export class NumericEditor extends NTextEditor {
|
||||
constructor(protected options: FieldOptions) {
|
||||
if (!options.editValue && typeof options.cellValue === 'number') {
|
||||
// If opening a number for editing, we render it using the basic string representation (e.g.
|
||||
// no currency symbols or groupings), but it's important to use the right locale so that the
|
||||
// number can be parsed back (e.g. correct decimal separator).
|
||||
const locale = options.field.documentSettings.peek().locale;
|
||||
const fmt = new Intl.NumberFormat(locale, {useGrouping: false, maximumFractionDigits: 20});
|
||||
const editValue = fmt.format(options.cellValue);
|
||||
options = {...options, editValue};
|
||||
}
|
||||
super(options);
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export const typeDefs: any = {
|
||||
widgets: {
|
||||
TextBox: {
|
||||
cons: 'NumericTextBox',
|
||||
editCons: 'TextEditor',
|
||||
editCons: 'NumericEditor',
|
||||
icon: 'FieldTextbox',
|
||||
options: {
|
||||
alignment: 'right',
|
||||
@@ -98,7 +98,7 @@ export const typeDefs: any = {
|
||||
},
|
||||
Spinner: {
|
||||
cons: 'Spinner',
|
||||
editCons: 'TextEditor',
|
||||
editCons: 'NumericEditor',
|
||||
icon: 'FieldSpinner',
|
||||
options: {
|
||||
alignment: 'right',
|
||||
|
||||
@@ -15,6 +15,7 @@ import {NewAbstractWidget} from 'app/client/widgets/NewAbstractWidget';
|
||||
import {NewBaseEditor} from 'app/client/widgets/NewBaseEditor';
|
||||
import {NTextBox} from 'app/client/widgets/NTextBox';
|
||||
import {NTextEditor} from 'app/client/widgets/NTextEditor';
|
||||
import {NumericEditor} from 'app/client/widgets/NumericEditor';
|
||||
import {NumericTextBox} from 'app/client/widgets/NumericTextBox';
|
||||
import {Reference} from 'app/client/widgets/Reference';
|
||||
import {ReferenceEditor} from 'app/client/widgets/ReferenceEditor';
|
||||
@@ -32,6 +33,7 @@ export const nameToWidget = {
|
||||
'TextBox': NTextBox,
|
||||
'TextEditor': NTextEditor,
|
||||
'NumericTextBox': NumericTextBox,
|
||||
'NumericEditor': NumericEditor,
|
||||
'HyperLinkTextBox': HyperLinkTextBox,
|
||||
'HyperLinkEditor': HyperLinkEditor,
|
||||
'Spinner': Spinner,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import {DocumentSettings} from 'app/common/DocumentSettings';
|
||||
import {getDistinctValues} from 'app/common/gutil';
|
||||
import {getCurrency, NumberFormatOptions, NumMode, parseNumMode} from 'app/common/NumberFormat';
|
||||
import {buildNumberFormat} from 'app/common/NumberFormat';
|
||||
import escapeRegExp = require('lodash/escapeRegExp');
|
||||
import last = require('lodash/last');
|
||||
|
||||
@@ -72,7 +73,7 @@ export default class NumberParse {
|
||||
// with corresponding digits from 0123456789.
|
||||
private readonly _replaceDigits: (s: string) => string;
|
||||
|
||||
constructor(locale: string, currency: string) {
|
||||
constructor(public readonly locale: string, public readonly currency: string) {
|
||||
const parts = new Map<NumMode, Intl.NumberFormatPart[]>();
|
||||
for (const numMode of NumMode.values) {
|
||||
const formatter = Intl.NumberFormat(locale, parseNumMode(numMode, currency));
|
||||
@@ -316,6 +317,12 @@ export default class NumberParse {
|
||||
result.decimals = decimals;
|
||||
}
|
||||
|
||||
// We should only set maxDecimals if the default maxDecimals is too low.
|
||||
const tmpNF = buildNumberFormat(result, {locale: this.locale, currency: this.currency}).resolvedOptions();
|
||||
if (maxDecimals > tmpNF.maximumFractionDigits) {
|
||||
result.maxDecimals = maxDecimals;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@ abstract class ValueGuesser<T> {
|
||||
|
||||
const parsed = this.parse(value);
|
||||
// Give up if too many strings failed to parse or if the parsed value changes when converted back to text
|
||||
if (typeof parsed === "string" && ++unparsed > maxUnparsed || formatter.formatAny(parsed) !== value) {
|
||||
if ((typeof parsed === "string" && ++unparsed > maxUnparsed) ||
|
||||
!this.isEqualFormatted(formatter.formatAny(parsed), value)) {
|
||||
return null;
|
||||
}
|
||||
result.push(parsed);
|
||||
@@ -83,6 +84,10 @@ abstract class ValueGuesser<T> {
|
||||
protected allowBlank(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected isEqualFormatted(formatted1: string, formatted2: string): boolean {
|
||||
return formatted1 === formatted2;
|
||||
}
|
||||
}
|
||||
|
||||
class BoolGuesser extends ValueGuesser<boolean> {
|
||||
@@ -126,6 +131,14 @@ class NumericGuesser extends ValueGuesser<number> {
|
||||
public parse(value: string): number | string {
|
||||
return this._parser.cleanParse(value);
|
||||
}
|
||||
|
||||
protected isEqualFormatted(formatted1: string, formatted2: string): boolean {
|
||||
// Consider format guessing successful if it returns the typed-in numeric value exactly or
|
||||
// differing only in whitespace.
|
||||
formatted1 = formatted1.replace(NumberParse.removeCharsRegex, "");
|
||||
formatted2 = formatted2.replace(NumberParse.removeCharsRegex, "");
|
||||
return formatted1 === formatted2;
|
||||
}
|
||||
}
|
||||
|
||||
class DateGuesser extends ValueGuesser<number> {
|
||||
|
||||
Reference in New Issue
Block a user