[IMP] web: allow to parse more numbers
The present commit reintroduces several number parsers that were previously defined in web.field_utils: parseFloatTime, parseInteger, parseMonetary, and parsePercentage. Some minor simplifications in their API have been brought. Notably, the "field" parameter has been removed. Indeed, we expect those parsers to be used in more specific contexts than before so that the advantage of having a common API (value, field, options) has become less important.
This commit is contained in:
committed by
Aaron Bohy
parent
a688f13cd4
commit
f4160d5907
@@ -1,9 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { escapeRegExp, intersperse, sprintf } from "../utils/strings";
|
||||
import { escapeRegExp, intersperse } from "../utils/strings";
|
||||
import { localization } from "./localization";
|
||||
import { _lt } from "./translation";
|
||||
|
||||
class InvalidNumberError extends Error {}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -32,7 +34,7 @@ function insertThousandsSep(num, thousandsSep = ",", grouping = [3, 0]) {
|
||||
* @param {Object} options - additional options
|
||||
* @param {string|RegExp} [options.thousandsSep] - the thousands separator used in the value
|
||||
* @param {string|RegExp} [options.decimalPoint] - the decimal point used in the value
|
||||
* @returns number
|
||||
* @returns {number}
|
||||
*/
|
||||
function parseNumber(value, options = {}) {
|
||||
// a number can have the thousand separator multiple times. ex: 1,000,000.00
|
||||
@@ -57,7 +59,7 @@ function parseNumber(value, options = {}) {
|
||||
* @param {number[]} [options.grouping]
|
||||
* array of relative offsets at which to insert `thousandsSep`.
|
||||
* See `numbers.insertThousandsSep` method.
|
||||
* @returns string
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatFloat(value, options = {}) {
|
||||
if (value === false) {
|
||||
@@ -72,11 +74,10 @@ export function formatFloat(value, options = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a more readable formated string representing a float to a float
|
||||
* Try to extract a float from a string. The localization is considered in the process.
|
||||
*
|
||||
* @param {string} value the formatted number
|
||||
* @returns float
|
||||
* @throws Error if the value can't be parsed properly
|
||||
* @param {string} value
|
||||
* @returns {number} a float
|
||||
*/
|
||||
export function parseFloat(value) {
|
||||
let thousandsSepRegex = new RegExp(escapeRegExp(localization.thousandsSep), "g");
|
||||
@@ -86,11 +87,104 @@ export function parseFloat(value) {
|
||||
decimalPoint: decimalPointRegex,
|
||||
});
|
||||
if (isNaN(parsed)) {
|
||||
throw new Error(sprintf(_lt("'%s' is not a correct float").toString(), value));
|
||||
throw new InvalidNumberError(`"${value}" is not a correct number`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to extract a float time from a string. The localization is considered in the process.
|
||||
* The float time can have two formats: float or integer:integer.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {number} a float
|
||||
*/
|
||||
export function parseFloatTime(value) {
|
||||
let sign = 1;
|
||||
if (value[0] === "-") {
|
||||
value = value.slice(1);
|
||||
sign = -1;
|
||||
}
|
||||
const values = value.split(":");
|
||||
if (values.length > 2) {
|
||||
throw new InvalidNumberError(`"${value}" is not a correct number`);
|
||||
}
|
||||
if (values.length === 1) {
|
||||
return sign * parseFloat(value);
|
||||
}
|
||||
const hours = parseInteger(values[0]);
|
||||
const minutes = parseInteger(values[1]);
|
||||
return sign * (hours + minutes / 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to extract an integer from a string. The localization is considered in the process.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {number} an integer
|
||||
*/
|
||||
export function parseInteger(value) {
|
||||
let thousandsSepRegex = new RegExp(escapeRegExp(localization.thousandsSep), "g");
|
||||
let decimalPointRegex = new RegExp(escapeRegExp(localization.decimalPoint), "g");
|
||||
const parsed = parseNumber(value, {
|
||||
thousandsSep: thousandsSepRegex,
|
||||
decimalPoint: decimalPointRegex,
|
||||
});
|
||||
if (!Number.isInteger(parsed)) {
|
||||
throw new InvalidNumberError(`"${value}" is not a correct number`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to extract a monetary value from a string. The localization is considered in the process.
|
||||
* The monetary value can have the formats sym$ float, float$ sym or float
|
||||
* where $ is a non breaking space and sym is a currency symbol.
|
||||
* If a symbol is found it must correspond to the default currency symbol or to the
|
||||
* symbol of the currency whose id is passed in options.
|
||||
*
|
||||
* @param {string} value
|
||||
* @param {Object} [options={}]
|
||||
* @param {number} [options.currencyId]
|
||||
* @returns {number} float
|
||||
*/
|
||||
export function parseMonetary(value, options = {}) {
|
||||
let values = value.split(" ");
|
||||
if (values.length === 1) {
|
||||
return parseFloat(value);
|
||||
}
|
||||
let currency;
|
||||
if (options.currencyId) {
|
||||
currency = odoo.session_info.currencies[options.currencyId];
|
||||
} else {
|
||||
currency = Object.values(odoo.session_info.currencies)[0];
|
||||
}
|
||||
const symbolIndex = values.findIndex((v) => v === currency.symbol);
|
||||
if (symbolIndex === -1) {
|
||||
throw new InvalidNumberError(`"${value}" is not a correct number`);
|
||||
}
|
||||
values.splice(symbolIndex, 1);
|
||||
if (values.length !== 1) {
|
||||
throw new InvalidNumberError(`"${value}" is not a correct number`);
|
||||
}
|
||||
return parseFloat(values[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to extract a float from a string and unconvert it with a conversion factor of 100.
|
||||
* The localization is considered in the process.
|
||||
* The percentage can have two formats: float or float%.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {number} float
|
||||
*/
|
||||
export function parsePercentage(value) {
|
||||
if (value[value.length - 1] === "%") {
|
||||
value = value.slice(0, value.length - 1);
|
||||
}
|
||||
return parseFloat(value) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number to a human readable format.
|
||||
* By example, 3000 could become 3k. Or massive number can use the scientific exponential notation.
|
||||
@@ -102,7 +196,7 @@ export function parseFloat(value) {
|
||||
* the minimum number of digits to preserve when switching to another
|
||||
* level of thousands (e.g. with a value of '2', 4321 will still be
|
||||
* represented as 4321 otherwise it will be down to one digit (4k))
|
||||
* @returns string
|
||||
* @returns {string}
|
||||
*/
|
||||
export function humanNumber(number, options = { decimals: 0, minDigits: 1 }) {
|
||||
number = Math.round(number);
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
|
||||
import { formatFloat, humanNumber, parseFloat } from "@web/core/l10n/numbers";
|
||||
import { defaultLocalization } from "../../helpers/mock_services";
|
||||
import {
|
||||
formatFloat,
|
||||
humanNumber,
|
||||
parseFloat,
|
||||
parseFloatTime,
|
||||
parseInteger,
|
||||
parseMonetary,
|
||||
parsePercentage,
|
||||
} from "@web/core/l10n/numbers";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
|
||||
function expectInvalidNumberError(assert, func, value, options) {
|
||||
let message = `${func.name} fails on value: "${value}"`;
|
||||
if (options) {
|
||||
message += ` with options: ${JSON.stringify(options)}`;
|
||||
}
|
||||
assert.throws(() => func(value, options), message);
|
||||
}
|
||||
|
||||
QUnit.module("utils", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
hooks.beforeEach(() => {
|
||||
patchWithCleanup(localization, defaultLocalization);
|
||||
});
|
||||
|
||||
@@ -69,16 +85,104 @@ QUnit.module("utils", (hooks) => {
|
||||
assert.strictEqual(parseFloat("1,000.00"), 1000);
|
||||
assert.strictEqual(parseFloat("1,000,000.00"), 1000000);
|
||||
assert.strictEqual(parseFloat("1,234.567"), 1234.567);
|
||||
assert.throws(
|
||||
() => parseFloat("1.000.000"),
|
||||
"Throw an exception if it's not a valid number"
|
||||
);
|
||||
expectInvalidNumberError(assert, parseFloat, "1.000.000");
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
assert.strictEqual(parseFloat("1.234,567"), 1234.567);
|
||||
assert.throws(
|
||||
() => parseFloat("1,000,000"),
|
||||
"Throw an exception if it's not a valid number"
|
||||
);
|
||||
expectInvalidNumberError(assert, parseFloat, "1,000,000");
|
||||
});
|
||||
|
||||
QUnit.test("parseFloatTime", function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
assert.strictEqual(parseFloatTime("0"), 0);
|
||||
assert.strictEqual(parseFloatTime("100"), 100);
|
||||
assert.strictEqual(parseFloatTime("100.00"), 100);
|
||||
assert.strictEqual(parseFloatTime("7:15"), 7.25);
|
||||
assert.strictEqual(parseFloatTime("-4:30"), -4.5);
|
||||
assert.strictEqual(parseFloatTime(":"), 0);
|
||||
assert.strictEqual(parseFloatTime("1:"), 1);
|
||||
assert.strictEqual(parseFloatTime(":12"), 0.2);
|
||||
|
||||
expectInvalidNumberError(assert, parseFloatTime, "a:1");
|
||||
expectInvalidNumberError(assert, parseFloatTime, "1:a");
|
||||
expectInvalidNumberError(assert, parseFloatTime, "1:1:");
|
||||
expectInvalidNumberError(assert, parseFloatTime, ":1:1");
|
||||
});
|
||||
|
||||
QUnit.test("parseInteger", function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
assert.strictEqual(parseInteger(""), 0);
|
||||
assert.strictEqual(parseInteger("0"), 0);
|
||||
assert.strictEqual(parseInteger("100"), 100);
|
||||
assert.strictEqual(parseInteger("-100"), -100);
|
||||
assert.strictEqual(parseInteger("1,000"), 1000);
|
||||
assert.strictEqual(parseInteger("1,000,000"), 1000000);
|
||||
expectInvalidNumberError(assert, parseInteger, "1.000.000");
|
||||
expectInvalidNumberError(assert, parseInteger, "1,234.567");
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
|
||||
assert.strictEqual(parseInteger("1.000.000"), 1000000);
|
||||
expectInvalidNumberError(assert, parseInteger, "1,000,000");
|
||||
expectInvalidNumberError(assert, parseInteger, "1.234,567");
|
||||
});
|
||||
|
||||
QUnit.test("parseMonetary", function (assert) {
|
||||
assert.expect(17);
|
||||
|
||||
patchWithCleanup(odoo.session_info, {
|
||||
currencies: {
|
||||
1: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "€",
|
||||
},
|
||||
3: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(parseMonetary(""), 0);
|
||||
assert.strictEqual(parseMonetary("0"), 0);
|
||||
assert.strictEqual(parseMonetary("100.00 €"), 100);
|
||||
assert.strictEqual(parseMonetary("-100.00"), -100);
|
||||
assert.strictEqual(parseMonetary("1,000.00"), 1000);
|
||||
assert.strictEqual(parseMonetary("1,000,000.00"), 1000000);
|
||||
assert.strictEqual(parseMonetary("$ 125.00", { currencyId: 3 }), 125);
|
||||
assert.strictEqual(parseMonetary("1,000.00 €", { currencyId: 1 }), 1000);
|
||||
|
||||
expectInvalidNumberError(assert, parseMonetary, " ", { currencyId: 3 });
|
||||
expectInvalidNumberError(assert, parseMonetary, "1 ", { currencyId: 3 });
|
||||
expectInvalidNumberError(assert, parseMonetary, " 1", { currencyId: 3 });
|
||||
|
||||
expectInvalidNumberError(assert, parseMonetary, "12.00 €");
|
||||
expectInvalidNumberError(assert, parseMonetary, "$ 12.00", { currencyId: 3 });
|
||||
expectInvalidNumberError(assert, parseMonetary, "1 $", { currencyId: 1 });
|
||||
expectInvalidNumberError(assert, parseMonetary, "$ 1"); // "€" is the default currency here
|
||||
|
||||
expectInvalidNumberError(assert, parseMonetary, "1$ 1", { currencyId: 1 });
|
||||
expectInvalidNumberError(assert, parseMonetary, "$ 12.00 34", { currencyId: 3 });
|
||||
});
|
||||
|
||||
QUnit.test("parsePercentage", function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
assert.strictEqual(parsePercentage(""), 0);
|
||||
assert.strictEqual(parsePercentage("0"), 0);
|
||||
assert.strictEqual(parsePercentage("0.5"), 0.005);
|
||||
assert.strictEqual(parsePercentage("1"), 0.01);
|
||||
assert.strictEqual(parsePercentage("100"), 1);
|
||||
assert.strictEqual(parsePercentage("50%"), 0.5);
|
||||
expectInvalidNumberError(assert, parsePercentage, "50%40");
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
|
||||
assert.strictEqual(parsePercentage("1.234,56"), 12.3456);
|
||||
assert.strictEqual(parsePercentage("6,02"), 0.0602);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user