es.davy.ai

Preguntas y respuestas de programación confiables

¿Tienes una pregunta?

Si tienes alguna pregunta, puedes hacerla a continuación o ingresar lo que estás buscando.

Cómo generar un valor vacío a partir de resultados agregados con Mongodb

En primer lugar, la función:

static async getInitRegistrationMetric(account) {
  // queremos solo el día completo, así que excluimos la fecha actual
  const exclude = DateTime.now()
    .setZone('utc')
    .startOf('day')
    .toISODate();

  // esto cuenta el número de clientes creados por día
  const groupByDate = {
    $group: {
      _id: {
        $dateToString: { format: '%Y-%m-%d', date: '$createdAt' },
      },
      count: { $sum: 1 },
    },
  };

  // esta es una forma de renombrar (_id, count) a (date, value)
  const renameData = {
    $project: {
      _id: 0,
      date: '$_id',
      value: '$count',
    },
  };

  // esto se realiza para filtrar los datos, quiero eliminar la fecha nula y el resultado de hoy
  const excludeTodayAndNull = {
    $match: {
      $and: [
        {
          date: {
            $ne: exclude,
          },
        },
        {
          date: {
            $ne: null,
          },
        },
      ],
    },
  };

  // account es el modelo mongoose.
  return account.aggregate([groupByDate, renameData, excludeTodayAndNull]);
}

Este código producirá datos como estos:

const data = [
  { date: '2000-10-01', value: 50 },
  { date: '2000-10-03', value: 12 },
  { date: '2000-10-07', value: 112 },
];

El problema es que no tengo valores para el 2º, 4º, 5º y 6º del mes. Mi idea era obligar a Mongo a “crear” valores vacíos para los otros días de la siguiente manera:

const data = [
  { date: '2000-10-01', value: 50 },
  { date: '2000-10-02', value: 0 },
  { date: '2000-10-03', value: 12 },
  { date: '2000-10-04', value: 0 },
  { date: '2000-10-05', value: 0 },
  { date: '2000-10-06', value: 0 },
  { date: '2000-10-07', value: 112 },
];

¿Cómo puedo pedirle a “aggregate” que complete la brecha entre fechas significativas con datos con 0 como valor?

Gracias.

PD: Ya lo hice por código en JS, pero se ve pesado y feo. Intento hacerlo de forma más limpia.

Tags:  , , , ,

Answer

  1. Avatar for davy.ai

    Para rellenar los vacíos entre las fechas significativas con un valor de 0 como dato, puedes utilizar el operador $setUnion en MongoDB.

    Primero, crea una colección de todas las fechas distintas dentro del rango especificado utilizando el operador $range.

    Luego, utiliza el operador $setUnion para combinar el resultado de la agregación con la colección recién creada y obtener todas las fechas en un solo array.

    Finalmente, utiliza los operadores $map y $ifNull para verificar si la fecha existe en el resultado original de la agregación y reemplazar el valor por 0 si no existe.

    Aquí tienes un ejemplo de implementación de la solución:

    static async getInitRegistrationMetric(account) {
      const start = DateTime.now().setZone('utc').minus({ days: 7 }); // para los últimos 7 días
      const end = DateTime.now().setZone('utc').minus({ days: 1 }); // excluyendo el día actual
    
      // Crea una colección de todas las fechas distintas dentro del rango especificado
      const allDates = {
        $range: [
          { $toDate: start.toISO() },
          { $add: [{ $toDate: end.toISO() }, 86400000] }, // añade un día para incluir la fecha de fin
          86400000 // 24 horas en milisegundos
        ]
      };
    
      // Agrupa por fecha y cuenta el número de registros
      const groupByDate = {
        $group: {
          _id: {
            $dateToString: { format: '%Y-%m-%d', date: '$createdAt'},
          },
          count: { $sum: 1 },
        },
      };
    
      // Proyecta para renombrar campos y convertir la fecha al formato ISO
      const renameData = {
        $project: {
          _id: 0,
          date: { $dateFromString: { dateString: '$_id', format: '%Y-%m-%d' } },
          value: '$count',
        },
      };
    
      // Combina el resultado de la agregación con la colección de todas las fechas dentro del rango especificado para obtener todas las fechas en un solo array
      const mergeDates = {
        $setUnion: ['$data', [allDates]],
      };
    
      // Mapea sobre el array combinado y reemplaza el valor por 0 si la fecha no existe en el resultado original de la agregación
      const fillMissingDates = {
        $project: {
          data: {
            $map: {
              input: mergeDates,
              as: 'd',
              in: {
                $mergeObjects: [
                  { date: '$$d' },
                  { $ifNull: [{ $arrayElemAt: [{ $filter: { input: '$data', cond: { $eq: ['$$this.date', '$$d'] } } }, 0] }, { value: 0 }] } // verifica si la fecha existe en el resultado original de la agregación y reemplaza el valor por 0 si no existe.
                ]
              }
            }
          }
        }
      };
    
      // Desdobla el array combinado para obtener el resultado final
      const unwindData = {
        $unwind: '$data',
      };
    
      // Devuelve el resultado final
      return account.aggregate([
        { $match: { createdAt: { $gte: start.toJSDate(), $lt: end.toJSDate() } } },
        groupByDate,
        renameData,
        { $group: { _id: null, data: { $push: '$$ROOT' } } },
        { $project: { data: 1, _id: 0 } },
        fillMissingDates,
        unwindData,
        { $replaceRoot: { newRoot: '$data' } },
      ]);
    }
    

    Ten en cuenta que el código anterior asume que deseas llenar los vacíos con 0 para un rango de fechas determinado, que puedes definir cambiando las variables start y end. Si deseas llenar los vacíos para un mes determinado, por ejemplo, puedes ajustar las variables start y end en consecuencia.

Comments are closed.