diff --git a/modules/core/localizer.js b/modules/core/localizer.js index f8e5a6df9..9e7d9850c 100644 --- a/modules/core/localizer.js +++ b/modules/core/localizer.js @@ -424,15 +424,28 @@ export function coreLocalizer() { return code; // if not found, use the code }; + /** + * Returns a function that formats a floating-point number in the given + * locale. + */ localizer.floatFormatter = (locale) => { if (!('Intl' in window && 'NumberFormat' in Intl && 'formatToParts' in Intl.NumberFormat.prototype)) { - return (number) => number.toString(); + return (number, fractionDigits) => { + return fractionDigits === undefined ? number.toString() : number.toFixed(fractionDigits); + }; } else { - return (number) => number.toLocaleString(locale, { maximumFractionDigits: 20 }); + return (number, fractionDigits) => number.toLocaleString(locale, { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits === undefined ? 20 : fractionDigits, + }); } }; + /** + * Returns a function that parses a number formatted according to the given + * locale as a floating-point number. + */ localizer.floatParser = (locale) => { // https://stackoverflow.com/a/55366435/4585461 const polyfill = (string) => parseFloat(string.trim()); @@ -460,5 +473,32 @@ export function coreLocalizer() { }; }; + /** + * Returns a function that returns the number of decimal places in a + * formatted number string. + */ + localizer.decimalPlaceCounter = (locale) => { + var literal, group, decimal; + if ('Intl' in window && 'NumberFormat' in Intl) { + const format = new Intl.NumberFormat(locale, { maximumFractionDigits: 20 }); + if (('formatToParts' in format)) { + const parts = format.formatToParts(-12345.6); + const literalPart = parts.find(d => d.type === 'literal'); + literal = literalPart && new RegExp(`[${literalPart.value}]`, 'g'); + const groupPart = parts.find(d => d.type === 'group'); + group = groupPart && new RegExp(`[${groupPart.value}]`, 'g'); + const decimalPart = parts.find(d => d.type === 'decimal'); + decimal = decimalPart && new RegExp(`[${decimalPart.value}]`); + } + } + return (string) => { + string = string.trim(); + if (literal) string = string.replace(literal, ''); + if (group) string = string.replace(group, ''); + const parts = string.split(decimal || '.'); + return parts && parts[1] && parts[1].length || 0; + }; + }; + return localizer; } diff --git a/modules/ui/fields/input.js b/modules/ui/fields/input.js index 45e48d9de..7bbfd5380 100644 --- a/modules/ui/fields/input.js +++ b/modules/ui/fields/input.js @@ -34,6 +34,7 @@ export function uiFieldText(field, context) { const isDirectionField = field.key.split(':').some(keyPart => keyPart === 'direction'); const formatFloat = localizer.floatFormatter(localizer.languageCode()); const parseLocaleFloat = localizer.floatParser(localizer.languageCode()); + const countDecimalPlaces = localizer.decimalPlaceCounter(localizer.languageCode()); if (field.type === 'tel') { fileFetcher.get('phone_formats') @@ -155,8 +156,7 @@ export function uiFieldText(field, context) { num = ((num % 360) + 360) % 360; } // make sure no extra decimals are introduced - const numDecimals = v.includes('.') ? v.split('.')[1].length : 0; - return formatFloat(clamped(num).toFixed(numDecimals)); + return formatFloat(clamped(num), countDecimalPlaces(v)); }); input.node().value = vals.join(';'); change()(); diff --git a/test/spec/core/localizer.js b/test/spec/core/localizer.js index 6a77e0ef8..01e2f20e7 100644 --- a/test/spec/core/localizer.js +++ b/test/spec/core/localizer.js @@ -6,6 +6,15 @@ describe('iD.coreLocalizer', function() { expect(selection.selectChild().classed('localized-text')).to.be.true; }); }); + describe('#floatFormatter', function () { + it('uses the specified number of fraction digits', function () { + var localizer = iD.coreLocalizer(); + var formatFloat = localizer.floatFormatter('en'); + expect(formatFloat(-0.1)).to.eql('-0.1'); + expect(formatFloat(-0.1, 0)).to.eql('-0'); + expect(formatFloat(-0.1, 2)).to.eql('-0.10'); + }); + }); describe('#floatParser', function () { it('roundtrips English numbers', function () { var localizer = iD.coreLocalizer(); @@ -58,4 +67,14 @@ describe('iD.coreLocalizer', function() { expect(parseFloat(formatFloat(3.14159))).to.eql(3.14159); }); }); + describe('#decimalPlaceCounter', function () { + it('counts decimal places in English numbers', function () { + var localizer = iD.coreLocalizer(); + var countDecimalPlaces = localizer.decimalPlaceCounter('en'); + expect(countDecimalPlaces('-0')).to.eql(0); + expect(countDecimalPlaces('-0.1')).to.eql(1); + expect(countDecimalPlaces('1.234')).to.eql(3); + expect(countDecimalPlaces('10')).to.eql(0); + }); + }); });