使用MongoDB聚合框架将数字保留两位小数

29

我正在使用mongodb聚合框架,并按如下所示进行一些计算

db.RptAgg.aggregate( 
  {$group: {
    _id: {Region: "$RegionTxt", Mth: "$Month"},           
    ActSls: {$sum:"$ActSls"},
    PlnSls: {$sum:"$PlnSls"}
  }},
  {$project: {
    ActSls: 1,
    PlnSls: 1,
    ActToPln: {$cond: [
      {$ne: ["$PlnSls", 0]}, 
      {$multiply: [{$divide: ['$ActSls', '$PlnSls']}, 100]}, 
      0
    ]}
  }}
); 

我正在尝试找出最好、最简单的方法将我的结果四舍五入到小数点后两位。以下是我的结果:

{
  "result": [{
    "_id": {
      "Region": "East",
      "Mth": 201301
    },
    "ActSls": 72,
    "PlnSls": 102,
    "ActToPln": 70.58823529411765
  }],
  "ok": 1
}

我希望“ActToPln”显示70.59而不是“ActToPln”: 70.58823529411765,在聚合框架本身的结果中。 我想避免在我的应用程序中进行四舍五入。

您能帮忙吗?

以下是我使用的数据集。

{
    "_id" : ObjectId("51d67ef69557c507cb172572"),
    "RegionTxt" : "East",
    "Month" : 201301,
    "Date" : "2013-01-01",
    "ActSls" : 31,
    "PlnSls" : 51
}
{
    "_id" : ObjectId("51d67ef69557c507cb172573"),
    "RegionTxt" : "East",
    "Month" : 201301,
    "Date" : "2013-01-02",
    "ActSls" : 41,
    "PlnSls" : 51
}

提前感谢。Nandu


你应该提供一些例子或数据来简化你的问题。 - Artem Mezhenin
我已更新问题并提供了数据集。感谢amezhenin。 - user2552537
4
为什么不在客户端做呢?在聚合框架中没有自然的方法来完成这个操作。你需要使用Map Reduce来执行取整功能。 - WiredPrairie
10个回答

17

虽然没有 $round 运算符,但您可以在聚合框架中完成这个操作 - 按照特定顺序进行操作通常可以避免浮点精度问题。

> db.a.save({x:1.23456789})
> db.a.save({x:9.87654321})
> db.a.aggregate([{$project:{ _id:0, 
         y:{$divide:[
              {$subtract:[
                      {$multiply:['$x',100]},
                      {$mod:[{$multiply:['$x',100]}, 1]}
              ]},
              100]}
}}])
{ "y" : 1.23 }
{ "y" : 9.87 }

在给定的问题中替换现有的管道:

{$multiply:[{$divide: ['$ActSls', '$PlnSls']},100]}

{$divide:[
     {$subtract:[ 
          {$multiply:[
             {$divide: ['$ActSls','$PlnSls']},
             10000
          ]}, 
          {$mod:[
             {$multiply:[{$divide: ['$ActSls','$PlnSls']}, 10000 ]},
             1]}
          ]}, 
     100
]}

使用您的样本数据点,这是结果:

{ "ActSls" : 31, "PlnSls" : 51, "ActToPln" : 60.78 }
{ "ActSls" : 41, "PlnSls" : 51, "ActToPln" : 80.39 }
{ "ActSls" : 72, "PlnSls" : 102, "ActToPln" : 70.58 }

2
请注意,这将技术上将结果截断为两位小数。如果您想实际四舍五入,应在最终除以100之前添加49。 - Asya Kamsky
Asya,请问你的第一个代码块中的变量/常量db.a.save({x:1.23456789})... 到底使用了哪个方法来对数字进行四舍五入?我指的是像C#中的Math.Round(123.234, 2)那样的方法,其中2是将数字舍入到小数点后几位。另外,请按照你上面提到的评论修改上述代码(在最后除以100之前加上49)。 - Faisal Mq
按步骤:给定一个数字 x,我将 x 乘以 100,并从 x100,1 的模数中减去(这是乘法后的小数部分)。那就留下了一个整数,是我想保留的数的100倍,然后我再除以100。1.5478100=>154.78 对其取余数并除以1得到 .78,所以减去后我得到了154,然后我将其除以100,剩下 1.54。对于小数点后不同位数的数字,请使用10的不同倍数代替100。 - Asya Kamsky
4
未来读者注意:从3.2版(2015年底)开始,聚合框架将具有三个可用于此操作的运算符:$trunc 可以将数值朝0截断,$floor 可以向下取整,$ceil 可以向上取整。 - Asya Kamsky

17

Mongo 4.2开始,新增了一个名为$round的聚合操作符,可以将一个数字按照给定精度四舍五入到指定小数位数:

{ $round : [ <number>, <place> ] }

可用于聚合管道中,以下示例将x保留两位小数:

// db.collection.insert([{x: 1.23456}, {x: 9.87654}, {x: 0.055543}, {x: 12.345}])
db.collection.aggregate([{ $project: { "rounded_x": { $round: ["$x", 2] }}}])
// [{"rounded_x": 1.23}, {"rounded_x": 9.88}, {"rounded_x": 0.06}, {"rounded_x": 12.35}]

请注意,place 参数是可选的,如果省略它,则舍入为整数(即保留 0 个小数位)。


2
请注意,这将进行“银行家舍入法”。如果您想要四舍五入/向上取整/向下取整/取零/取无穷大,则此方法不适用。 - GACy20
以上有所修改。$round阶段的功能非常奇怪。文档:https://www.mongodb.com/docs/manual/reference/operator/aggregation/round/#rounding-to-even-values - VitaminTussle

9

我不知道为什么,但是这个页面上所有的答案都给了我一个 12.34 的结果,而不是 12.345。因此,我自己写了一个项目阶段:

x = 12.345

{'$project': {
    y: {'$divide': [{'$trunc': {'$add': [{'$multiply': ['$x', 100]}, 0.5]}}, 100]},
}},

它给出了12.35


这是简单的算术,没有花招:

  1. 12.345 * 100 = 1234.5

    这一步将我们带到四舍五入的位置:100=10^2(小数点后两位)。该步骤将由第4步平衡回来。

  2. 1234.5 + 0.5 = 1235.0

    在这里我得到了我的四舍五入

  3. truncate(1235.0) = 1235

    只需删除小数部分。

  4. 1235 / 100 = 12.35

然而,对于负数它不起作用(这已足够进行聚合)。对于正数和负数,您都应该使用它与abs一起使用:

{'$project': {
    z: {'$multiply': [
        {'$divide': ['$x', {'$abs': '$x'}]}, 
        {'$divide': [{'$trunc': {'$add': [{'$multiply': [{'$abs': '$x'}, 100]}, 0.5]}}, 100]}
    ]},
}}

在这里,我获取数字的符号,通过取绝对值包装原始数字,然后将符号乘以舍入输出结果。


1
可能对你来说已经不相关了,但为了纪念,这是因为(出于一些绝对荒谬的原因),Mongo的$round阶段在检查到数字为5时会四舍五入到最近的偶数。请参见其行为的文档:https://www.mongodb.com/docs/manual/reference/operator/aggregation/round/#rounding-to-even-values - VitaminTussle
1
@VitaminTussle 这真是非常奇怪的行为,了解这一点非常重要,谢谢! - egvo

4

更新(2023年5月2日)

该库已不再维护,请考虑其他选项。


mongo-round 运行良好。

假设这个数字是 3.3333333

var round = require('mongo-round');

db.myCollection.aggregate([
    { $project: {
        roundAmount: round('$amount', 2)  // it will become 3.33
    } }
]);

4
此解决方案正确地四舍五入至小数点后两位:
"rounded" : {
  $subtract:[
    {$add:['$absolute',0.0049999999999999999]},
    {$mod:[{$add:['$absolute',0.0049999999999999999]}, 0.01]}
  ]
}

例如,它将1.2499四舍五入至1.25,但将1.2501向下舍入至1.25。
注:
1.该解决方案基于http://www.kamsky.org/stupid-tricks-with-mongodb/rounding-numbers-in-aggregation-framework中给出的示例。
2.它解决了Asya Kamsky答案中存在的问题,即仅截断而没有正确地四舍五入;即使在评论所建议的更改之后也是如此。
3.加法因子中尾随9的数量很大,以适应高精度输入数字。根据要四舍五入的数字的精度,加法因子可能需要更加精确。

2
当前版本的聚合框架中没有圆形运算符。您可以尝试使用以下代码片段:

> db.a.save({x:1.23456789})
> db.a.save({x:9.87654321})
> db.a.aggregate([{$project:{y:{$subtract:['$x',{$mod:['$x', 0.01]}]}}}])
{
    "result" : [
        {
            "_id" : ObjectId("51d72eab32549f94da161448"),
            "y" : 1.23
        },
        {
            "_id" : ObjectId("51d72ebe32549f94da161449"),
            "y" : 9.870000000000001
        }
    ],
    "ok" : 1
}

但是正如你所看到的,这种解决方案由于精度问题并不理想。在这种情况下,最简单的方法是遵循@wiredprairie的建议,在应用程序中进行round操作。


1
rounded:{'$multiply': [{ "$cond": [{ "$gte": [ "$x", 0 ] }, 1,-1 ]},{'$divide': [{'$trunc': {'$add': [{'$multiply': [{'$abs': '$x'}, {$pow:[10,2]}]}, 0.5]}}, {$pow:[10,2]}]}]}

egvo的解决方案很棒,但如果为零就会出现除以零的情况。为了避免这种情况,可以使用$cond检测符号。
(将x替换为field_name,将数字2替换为所需的小数位数)

1
请注意使用$round聚合时可能会出现问题。当使用double类型时,有时会错误地进行四舍五入。以下是一个示例:

5.885 --转换为double--> 5.8849999999999 ---$round(2)--> 5.88 (而不是5.89)

这里有一个解决方法:

> db.a.save({x:5.884})
> db.a.save({x:5.885})
> db.a.save({x:5.886})
> db.a.aggregate([{
     $project:{
         _id:0, 
         x: '$x',
         round: {$round: ['$x', 2]}
         better_round: {$round: [{$add: ['$x', 0.000000001]}, 2]}
     }
}])
Output:
{ "x" : 5.884, "round" : 5.88, "better_round" : 5.88 }
{ "x" : 5.885, "round" : 5.88, "better_round" : 5.89 }
{ "x" : 5.886, "round" : 5.89, "better_round" : 5.89 }

1
让我说一下,MongoDB没有这个函数真是遗憾。我希望他们能很快添加上。
然而,我想出了一个冗长的聚合管道。虽然它可能不太高效,但它遵守四舍五入规则。
db.t.aggregate([{
    $project: {
        _id: 0,
        number: {
            $let: {
                vars: {
                    factor: {
                        $pow: [10, 3]
                    },
                },
                in: {
                    $let: {
                        vars: {
                            num: {$multiply: ["$$factor", "$number"]},
                        },
                        in: {
                            $switch: {
                                branches: [
                                    {case: {$gte: ["$$num", {$add: [{$floor: "$$num"}, 0.5]}]}, then: {$divide:[ {$add: [{$floor: "$$num"}, 1.0]},"$$factor"]}},
                                    {case: {$lt: ["$$num", {$add: [{$floor: "$$num"}, 0.5]}]}, then: {$divide:[{$floor: "$$num"}, "$$factor"]}}                                    
                                ]
                            }
                        }
                    }
                }
            }
        }
    }
}])

假设我的集合中有以下名为t的文档:
{ number" : 2.341567 }
{ number" : 2.0012 }
{ number" : 2.0012223 }

运行上述查询后,我得到了:
{ "number" : 2.342 }
{ "number" : 2.001 }
{ "number" : 2.001 }

1
{$divide:[
            {$cond: { if: { $gte: [ {$mod:[{$multiply:['$dollarAmount',100]}, 1]}, 0.5 ] }, then: {$add: [{$subtract:[
                  {$multiply:['$dollarAmount',100]},
                  {$mod:[{$multiply:['$dollarAmount',100]}, 1]}
          ]}
                ,1]}, else: {$subtract:[
                  {$multiply:['$dollarAmount',100]},
                  {$mod:[{$multiply:['$dollarAmount',100]}, 1]}
          ]} }}
          , 
          100]}

希望这些能有所帮助,使得结果更加圆满。

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