user1120144
user1120144

Reputation:

Check if all elements in MongoDB array match a given query

Here's how my document looks:

nookstore = {
     "name": "Nook store",
     "categories": ["bookstore"],
     "working_hours": [
        {"opens": 8*60*60, "closes": 21*60*60 },
        {"opens": 8*60*60, "closes": 21*60*60 },
        {"opens": 8*60*60, "closes": 21*60*60 },
        {"opens": 8*60*60, "closes": 21*60*60 },
        {"opens": 8*60*60, "closes": 21*60*60 },
        {"opens": 8*60*60, "closes": 21*60*60 },
        {"opens": 9*60*60, "closes": 20*60*60 },
     ]
  }

  ...

kindlestore = {
     "name": "Kindle store",
     "categories": ["bookstore"],
     "working_hours": [
        {"opens": 0, "closes": 24*60*60 },
        {"opens": 0, "closes": 24*60*60 },
        {"opens": 0, "closes": 24*60*60 },
        {"opens": 0, "closes": 24*60*60 },
        {"opens": 0, "closes": 24*60*60 },
        {"opens": 0, "closes": 24*60*60 },
        {"opens": 0, "closes": 24*60*60 },
     ],
  }

I'm looking for the 24-hours stores, i.e. the stores in which every element of working_hours opens at 0 and closes at 24*60*60 (seconds after midnight).

I tried using $all and $elemMatch, but they work in cases where I need to match if at least one of the elements of an array matches a given criteria (which I don't need).

Upvotes: 3

Views: 2794

Answers (2)

Joachim Isaksson
Joachim Isaksson

Reputation: 180887

You could do it using the aggregation framework, probably in multiple ways. This is one that just groups to find unique open/close times and matches at the end on a single open/close time that's midnight to midnight.

db.stores.aggregate(
  [ 
    { $unwind: '$working_hours' },
    { $group: { _id: '$name', times: { $addToSet: '$working_hours' } } },
    { $unwind: '$times' },
    { $group: { _id: '$_id', times: { $push: '$times' }, cnt: { $sum: 1 } } },
    { $match: { 'times.opens': 0, 'times.closes': 86400, cnt: 1 } }
  ]
)

EDIT: Another, probably better alternative is to match as early as possible to not have to group values that aren't relevant;

db.stores.aggregate(
  [
    { $unwind: '$working_hours' },
    { $match: { 'working_hours.opens': 0, 'working_hours.closes': 86400 } },
    { $group: { _id: '$name', cnt: { $sum: 1 } } },
    { $match: { 'cnt': 7 } }
  ]
)

EDIT2: It can also be done without the aggregate framework by finding all stores that don't have a non matching time;

db.stores.find({
  'working_hours': { 
    $not: { 
      $elemMatch: {
        $or:[ { 'opens': { $ne: 0 } }, { 'closes': { $ne: 86400 } } ]
      }
    }
  }
}).pretty()

Upvotes: 1

WiredPrairie
WiredPrairie

Reputation: 59763

While I think this would be best if you modified the schema to include the special status as another field (open24hours), you could use the aggregation framework to find matches.

db.stores.aggregate({$unwind : '$working_hours' }, 
   { $group : { 
       _id : { 
         name: '$name', 
         opens: '$working_hours.opens', 
         closes: '$working_hours.closes' }, 
      total: { $sum : 1 }
      }
  }, 
  { $match : { total : 7 } })

This unwinds the hours opened, then groups on the name, opens and closes times, thereby making a unique combination, and then totals the number of matches for each combination. If there were 7 for the total, then it matched on all 7 days. You could do further matches to only find opens at 0 and closes at 86400.

Results:

{
    "result" : [
            {
                    "_id" : {
                            "name" : "Kindle store",
                            "opens" : 0,
                            "closes" : 86400
                    },
                    "total" : 7
            }
    ],
    "ok" : 1
}

Upvotes: 1

Related Questions