如何将带有逗号千位分隔符的字符串解析为数字?

165
我有一个字符串2,299.00,我想将其解析为数字。我尝试使用parseFloat,结果得到了2。我猜逗号是问题所在,但是应该如何正确地解决这个问题?只是移除逗号吗?
```javascript parseFloat("2,299.00".replace(",", "")); ```

var x = parseFloat("2,299.00")
console.log(x);

17个回答

192

是的,去掉逗号:

let output = parseFloat("2,299.00".replace(/,/g, ''));
console.log(output);


7
是的,但是现在小数位被省略了。例如,2300.00 的结果为 2300。 - user1540714
1
@user1540714 这是因为它是浮点数而不是字符串。如果您需要输出它,您需要将其格式化为始终显示2个小数点。 - Jon Taylor
能避免这个吗?我会尝试使用toFixed。 - user1540714
4
在法语环境中,逗号被用作小数分隔符...因此,如果浏览器设置为法语环境,则很可能会出现错误。 - Aqeel Ashiq
如果您从数据库、json编码的数据或其他数据源中获取原始数字作为字符串,那么我认为在转换为数字之前,它不会受到浏览器语言环境的影响,对吗?这意味着你可以像上面建议的那样进行字符串操作,以去除逗号,但是如果你在字符串上使用parseFloat(),则会依赖于区域设置。解决区域问题的一种方法是将小数点分隔的数字输出为(part1 + part2/100).toFixed(2) - Jon
在 ECMAScript 2021 中,我们可以使用 .replaceAll(",", "") - Sebastian Simon

184

删除逗号可能存在潜在危险,因为正如其他评论中提到的那样,许多地区使用逗号表示不同含义(例如小数点)。

我不知道您的字符串是从哪里得来的,但在世界上一些地方 "2,299.00" = 2.299

Intl 对象本可以是解决此问题的好方法,但不知何故,它们只发布了一个 Intl.NumberFormat.format() API而没有 parse 对应项 :(

以任何国际化数字字符方式将带有这些字符的字符串解析为机器可识别数字的唯一方法是使用利用 CLDR 数据覆盖所有可能格式化数字字符串的库。http://cldr.unicode.org/

到目前为止,我发现以下两个最好的 JS 选项:


6
真不敢相信还没有人点赞这个回答,它是这个页面上唯一的实际回答! - evilkos
19
完全同意应该有一个Intl的解析对应项。似乎很明显人们需要它。 - carlossless
1
这是唯一系统化的方法。无法通过一个适用于所有情况的正则表达式来实现。逗号和句号在不同语言中有不同的含义。 - Wildhammer
2
实际上,在世界上任何地方,"2,299.00" == 2.299 都是不成立的。任何明智的人都会看这个数字并假定它是 2299 格式化到小数点后两位,在一种使用逗号分组和句点/小数点表示小数的格式中。除非你告诉我有一个地方他们使用句点分组小数,并在 1 2/3 组后停止?我认为那是胡说八道。无论如何,你可以使用由 Intl.NumberFormat 公开的格式化规则来构建一个解析器:请参见 是否有任何 JavaScript 标准 API 可以根据区域设置解析为数字? 的答案。 - Heretic Monkey
@HereticMonkey 我不使用Closure,那是Google的库。CLDR有与我在答案中所述完全相同的解析规则,我链接的测试是使用CLDR和Google的库的测试。 - David Meister
显示剩余2条评论

64

在现代浏览器中,你可以使用内置的Intl.NumberFormat来检测浏览器数字格式,并将输入归一化以匹配。

function parseNumber(value, locales = navigator.languages) {
  const example = Intl.NumberFormat(locales).format('1.1');
  const cleanPattern = new RegExp(`[^-+0-9${ example.charAt( 1 ) }]`, 'g');
  const cleaned = value.replace(cleanPattern, '');
  const normalized = cleaned.replace(example.charAt(1), '.');

  return parseFloat(normalized);
}

const corpus = {
  '1.123': {
    expected: 1.123,
    locale: 'en-US'
  },
  '1,123': {
    expected: 1123,
    locale: 'en-US'
  },
  '2.123': {
    expected: 2123,
    locale: 'fr-FR'
  },
  '2,123': {
    expected: 2.123,
    locale: 'fr-FR'
  },
}


for (const candidate in corpus) {
  const {
    locale,
    expected
  } = corpus[candidate];
  const parsed = parseNumber(candidate, locale);

  console.log(`${ candidate } in ${ corpus[ candidate ].locale } == ${ expected }? ${ parsed === expected }`);
}

显然还有一些优化和缓存的空间,但这在所有语言中都可以可靠地工作。


1
为什么这个没有得到很多赞!!!这是最优雅的国际格式输入解决方案!谢谢!! - kpollock
navigator.languages 传递给构造函数比传递 navigator.language 更好,不是吗?我在想这个问题,因为在书籍《现代 JavaScript 快速入门》第180页中建议使用前者。虽然不确定两者之间的区别是什么。 - Kohei Nozaki
@KoheiNozaki,这个函数可以接受任何语言环境作为参数,因此理论上你可以在同一页上以多种方式格式化你的数字。navigator.language被用作合理的默认值,因为它应该是用户首选的语言。 - Paul Alexander
1
@KoheiNozaki 我再次查看了Locale negotiation,似乎你的想法是正确的。如果您将所有区域设置都传递给浏览器,它应该会协商首选区域设置,这样做不会有任何损失,反而可能更好。 - Paul Alexander
1
假设本地环境为 en-USparseNumber("12,00") 将返回 1200。如何改进代码以在字符串不正确时返回空字符串? - davidtingsu
显示剩余5条评论

28
注意:本方法无法处理科学计数法中的数字(例如表示一千的1e3)。
删除所有除数字、小数点和减号(-)以外的字符(如果需要支持一元正号+,则还可保留一个+)。 如果你可以假设小数点为十进制分隔符(但实际上在世界许多地方并非如此),那么代码可能如下所示:
function convertToFloat(str) {
    let body = str;
    let sign = "";
    const signMatch = /^\s*(-|\+)/.exec(str);
    // Or if you don't want to support unary +:
    // const signMatch = /^\s*(-)/.exec(str);
    if (signMatch) {
        body = str.substring(signMatch.index + 1);
        sign = signMatch[1];
    }
    const updatedBody = str.replace(/[^\d\.]/g, "");
    const num = parseFloat(sign + updatedBody);
    return num;
}

实时示例(我添加了一个小数部分以显示其工作方式):

function convertToFloat(str) {
    let body = str;
    let sign = "";
    const signMatch = /^\s*(-|\+)/.exec(str);
    // Or if you don't want to support unary +:
    // const signMatch = /^\s*(-)/.exec(str);
    if (signMatch) {
        body = str.substring(signMatch.index + 1);
        sign = signMatch[1];
    }
    const updatedBody = str.replace(/[^\d\.]/g, "");
    const num = parseFloat(sign + updatedBody);
    return num;
}

console.log(convertToFloat("2,299.23"));

如果您想支持小数点不是 . 的地区(有很多这样的地方),您可以检测小数点并将检测到的小数点用于正则表达式中。以下是查找小数点的示例函数:
function findDecimalSeparator() {
    const num = 1.2;
    if (typeof Intl === "object" && Intl && Intl.NumberFormat) {
        // I'm surprised it's this much of a pain and am hoping I'm missing
        // something in the API
        const formatter = new Intl.NumberFormat();
        const parts = formatter.formatToParts(num);
        const decimal = parts.find(({ type }) => type === "decimal").value;
        return decimal;
    }
    // Doesn't support `Intl.NumberFormat`, fall back to dodgy means
    const str = num.toLocaleString();
    const parts = /1(\D+)2/.exec(str);
    return parts[1];
}

然后,convertToFloat 看起来像这样:
const decimal = findDecimalSeparator();
function convertToFloat(str) {
    let body = str;
    let sign = "";
    const signMatch = /^\s*(-|\+)/.exec(str);
    // Or if you don't want to support unary +:
    // const signMatch = /^\s*(-)/.exec(str);
    if (signMatch) {
        body = str.substring(signMatch.index + 1);
        sign = signMatch[1];
    }
    const rex = new RegExp(`${escapeRegex(decimal)}|-|\\+|\\D`, "g");
    const updatedBody = body.replace(
        rex,
        (match) => match === decimal ? "." : ""
    );
    const num = parseFloat(sign + updatedBody);
    return num;
}

实例演示:

const decimal = findDecimalSeparator();

function findDecimalSeparator() {
    const num = 1.2;
    if (typeof Intl === "object" && Intl && Intl.NumberFormat) {
        // I'm surprised it's this much of a pain and am hoping I'm missing
        // something in the API
        const formatter = new Intl.NumberFormat();
        const parts = formatter.formatToParts(num);
        const decimal = parts.find(({ type }) => type === "decimal").value;
        return decimal;
    }
    // Doesn't support `Intl.NumberFormat`, fall back to dodgy means
    const str = num.toLocaleString();
    const parts = /1(\D+)2/.exec(str);
    return parts[1];
}

function escapeRegex(string) {
    return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, "\\$&");
}

function convertToFloat(str) {
    let body = str;
    let sign = "";
    const signMatch = /^\s*(-|\+)/.exec(str);
    // Or if you don't want to support unary +:
    // const signMatch = /^\s*(-)/.exec(str);
    if (signMatch) {
        body = str.substring(signMatch.index + 1);
        sign = signMatch[1];
    }
    const rex = new RegExp(`${escapeRegex(decimal)}|-|\\+|\\D`, "g");
    const updatedBody = body.replace(
        rex,
        (match) => match === decimal ? "." : ""
    );
    const num = parseFloat(sign + updatedBody);
    return num;
}

function gid(id) {
    const element = document.getElementById(id);
    if (!element) {
        throw new Error(`No element found for ID ${JSON.stringify(id)}`);
    }
    return element;
}

function onClick(id, handler) {
    gid(id).addEventListener("click", handler);
}

onClick("convert", () => {
    const str = gid("num").value;
    const num = convertToFloat(str);
    console.log(`${JSON.stringify(str)} => ${num}`);
});
<div>Enter a number using your locale's grouping and decimal separators, optionally prefaced with a minus sign (<code>-</code>) or plus sign (<code>+</code>):</div>
<input type="text" id="num" value="-123">
<input type="button" id="convert" value="Convert">


4
这种方法不适用于负数或科学计数法表示的数字。 - Aadit M Shah
1
str = str.replace(/(\d+),(?=\d{3}(\D|$))/g, "$1"); 这是我会用的,但我对正则表达式一窍不通,这是我在一些其他 SO 线程上找到的。 - Jon Taylor
1
删除“-”减号是一个坏主意 - 会得到完全不同的数字,而且如果解析异构格式,“7.500”!=“7,500”。 - serge
@Serge - 上面的代码并不会移除 -,它会保留它。OP 没有提到异构格式,只是使用 , 作为分组分隔符。 - T.J. Crowder
以上解决方案对我有效。我认为这是去除1000分隔符的最佳准确方式。谢谢@T.J.Crowder。 - Ansif
显示剩余5条评论

19
通常,对于数字值,您应该考虑使用不允许自由文本输入的输入字段。但是,在某些情况下,您需要猜测输入格式。例如,在德国,1.234,56表示美国的1,234.56。请参阅https://salesforce.stackexchange.com/a/21404以获取使用逗号作为小数的国家列表。
我使用以下函数来猜测最佳答案并剥离所有非数字字符:
function parseNumber(strg) {
    var strg = strg || "";
    var decimal = '.';
    strg = strg.replace(/[^0-9$.,]/g, '');
    if(strg.indexOf(',') > strg.indexOf('.')) decimal = ',';
    if((strg.match(new RegExp("\\" + decimal,"g")) || []).length > 1) decimal="";
    if (decimal != "" && (strg.length - strg.indexOf(decimal) - 1 == 3) && strg.indexOf("0" + decimal)!==0) decimal = "";
    strg = strg.replace(new RegExp("[^0-9$" + decimal + "]","g"), "");
    strg = strg.replace(',', '.');
    return parseFloat(strg);
}   

在这里尝试一下:https://plnkr.co/edit/9p5Y6H?p=preview

示例:

1.234,56 € => 1234.56
1,234.56USD => 1234.56
1,234,567€ => 1234567
1.234.567 => 1234567
1,234.567 => 1234.567
1.234 => 1234 // might be wrong - best guess
1,234 => 1234 // might be wrong - best guess
1.2345 => 1.2345
0,123 => 0.123

该函数存在一个弱点:如果你输入1,123或1.123,无法猜测其格式——这是因为根据本地格式,两者都可能是逗号或千分位符。在这种特殊情况下,该函数将把分隔符视为千分位符并返回1123。


它无法处理像1,111.11这样的数字,这显然是英文格式,但返回的是111111。 - Mr. Goferito
谢谢你,Goferito先生 - 我很抱歉 - 我修复了这个函数。 - ESP32
看起来在法国语环境下,非常小的数字“0.124”也可能失败。 - Paul Alexander
哇,太好了!这几乎就是我要找的:3,00&nbsp;€也被替换为3.00。只是需要注意的是,3,001格式化为3001。为了避免这种情况,输入应始终带有小数符号。例如,3,001.00€ 3,001.00可以正确转换。另外,请更新jsfiddle。那里的0,124仍然被转换为124。 - Arnis Juraga
干得好伙计,我认为这个答案值得成为正确答案。 - Waheed
很棒的函数。我建议在第三个if语句中增加一个检查:strg.indexOf(decimal) !== 0。这将捕获.123和,123的情况,并将它们与0.123和0,123的情况视为相同。 - sunnymtn

5
很难理解他们为什么会包含一个toLocaleString但是却没有一个parse方法。至少在IE6+,没有参数的toLocaleString被广泛支持。
对于国际化方案,我想到了以下方法:
首先检测用户的本地化小数分隔符:
var decimalSeparator = 1.1;
decimalSeparator = decimalSeparator.toLocaleString().substring(1, 2);

如果字符串中有多个小数分隔符,请将数字规范化:

var pattern = "([" + decimalSeparator + "])(?=.*\\1)";separator
var formatted = valor.replace(new RegExp(pattern, "g"), "");

最后,删除任何不是数字或小数分隔符的内容:

formatted = formatted.replace(new RegExp("[^0-9" + decimalSeparator + "]", "g"), '');
return Number(formatted.replace(decimalSeparator, "."));

4
Number("2,299.00".split(',').join(''));   // 2299

split函数使用“,”作为分隔符将字符串拆分为数组,并返回一个数组。
join函数将从split函数返回的数组元素合并起来。
Number()函数将合并后的字符串转换为数字。


请详细阐述解决方案是什么,以及它如何解决问题。 - Tariq
我喜欢这个解决方案。赞同它的简单易用。 - Yeti

3

或者尝试这种更短的方法:

const myNum =  +('2,299.00'.replace(",",""));

如果你有多个逗号,可以使用正则表达式:
const myNum =  +('2,022,233,988.55'.replace(/,/g,""));
// -> myNum = 2022233988.55

以下是与我类似的案例所使用的数组(用于类似的用例):

获取该数组的总和,可以按照以下方法进行操作:

const numbers = ["11", "7", "15/25", "18/5", "12", "16/25"]

通过使用 parseFloat 我可能会丢失小数点,为了得到精确的总和,我必须先将正斜杠替换为点,然后将字符串转换为实际数字。
所以:
const currectNumbers = numbers.map(num => +(num.replace("/",".")))

// or the longer approach:
const currectNumbers = numbers
.map(num => num.replace("/","."))
.map(num => parseFloat(num));

这将给我所需的数组,可用于reduce方法:
currectNumbers = [ 11, 7, 15.25, 18.5, 12, 16.25]

3
如果您想避免David Meister发布的问题,并且您确定小数点的位数,您可以替换所有点和逗号并除以100,例如:

var value = "2,299.00";
var amount = parseFloat(value.replace(/"|\,|\./g, ''))/100;

或者如果您有3个小数位

var value = "2,299.001";
var amount = parseFloat(value.replace(/"|\,|\./g, ''))/1000;

如果您想使用parseInt、parseFloat或Number,取决于您自己。此外,如果您想保留小数位数,可以使用函数.toFixed(...)。


2

如果数字是百万级别的,所有这些答案都会失败。

使用replace方法,3,456,789仅返回3456。

最正确的方法简单地删除逗号。

var number = '3,456,789.12';
number.split(',').join('');
/* number now equips 3456789.12 */
parseFloat(number);

或者简单写成。
number = parseFloat(number.split(',').join(''));

当然,美国人使用逗号而不是点。试图制作一个庞然大物来尝试处理两者是愚蠢的。 - Case
3
但是作为Unicode提供的一部分,这样的“畸形字符”已经存在(请参见我的回答)。如果你经营着一个拥有国际客户的公司,我相信你会觉得这样做不会让自己感到愚蠢。 - David Meister

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接