Home > database >  How to project a sum of a field in a sub-document based on a condition in MongoDB?
How to project a sum of a field in a sub-document based on a condition in MongoDB?

Time:12-20

I have a Customer collection with the following document:

{
  "_id": 1,
  firstname: "John",
  lastname: "Doe",
  credits: [
    {
      cDate: "2020-01-16",
      cAmount: 350
    },
    {
      cDate: "2021-02-07",
      cAmount: 180
    },
    {
      cDate: "2021-06-25",
      cAmount: 650
    },
  ]
}
{
  "_id": 2,
  firstname: "Bob",
  lastname: "Smith",
  credits: [
    {
      cDate: "2020-03-19",
      cAmount: 200
    },
    {
      cDate: "2020-08-20",
      cAmount: 90
    },
    {
      cDate: "2021-11-11",
      cAmount: 300
    },
  ]
}

Now I would like to return the total spent for a specific year i.e. 2021.
The data should look something like this:

{"firstname": "John", "lastname": "Doe", "total": 830},
{"firstname": "Bob", "lastname": "Smith", "total": 300}

First I tried to match the records that contain cDates within the expected year (2021) to reduce the number of records (the actual dataset has hundreds of customers) and then projected the wanted fields:

Customer.aggregate([
  {
    $match: {
      credits: {
        $elemMatch: {
          cDate: {
            $gte: ISODate("2021-01-01"),
            $lte: ISODate("2021-12-31"),
          },
        },
      },
    },
  },
  {
    $project: {      
      _id: 0,
      firstname: 1,
      lastname: 1,
      total: {
        $sum: "$credits.cAmount",
      },
    },
  }
])

the result is:

{"firstname": "John", "lastname": "Doe", "total": 1180},
{"firstname": "Bob", "lastname": "Smith", "total": 590}

Almost there, now I'd like to skip the credit records that do not contain the expected year (2021), so that only the values with a cDate equal to 2021 are calculated.
The $match I kept the same and I tried to add a $cond in the $project bit.

Customer.aggregate([
  {
    $match: {
      credits: {
        $elemMatch: {
          cDate: {
            $gte: ISODate("2021-01-01"),
            $lte: ISODate("2021-12-31"),
          },
        },
      },
    },
  },
  {
    $project: {      
      _id: 0,
      firstname: 1,
      lastname: 1,
      total: {        
        $cond: {
          if: { credits: { cDate: { regex: "2021-" } } }, // if cDate contains 2021-
          then: { $sum: "$credits.cAmount" },             // add the cAmount
          else: { $sum: 0 }                               // else add 0
        },
      },
    },
  }
])

This results is still the same, all totals get calulated from all years.

{"firstname": "John", "lastname": "Doe", "total": 1180},
{"firstname": "Bob", "lastname": "Smith", "total": 590}

What am I missing?
Thanks for your help.

CodePudding user response:

Property cDate has string value, you can not match by date type,

  • $match cDate by $regex and match "2021" year
  • $reduce to iterate loop of credits array, set initial value to 0
  • $substr to get substring of the cDate from 0 index and 4 character that is year
  • $cond to check is substring is "2021" then $sum the initial value with cAmount otherwise return initial value
Customer.aggregate([
  {
    $match: {
      "credits.cDate": {
        $regex: "2021"
      }
    }
  },
  {
    $project: {
      _id: 0,
      firstname: 1,
      lastname: 1,
      total: {
        $reduce: {
          input: "$credits",
          initialValue: 0,
          in: {
            $cond: [
              {
                $eq: [
                  { $substr: ["$$this.cDate", 0, 4] },
                  "2021"
                ]
              },
              { $sum: ["$$value", "$$this.cAmount"] },
              "$$value"
            ]
          }
        }
      }
    }
  }
])

Playground

  • Related