0
\$\begingroup\$

I have to turn an array of objects that look this this:

[
  { 
    "id": 15, 
    "log_name": "default", 
    "description": "updated", 
    "properties": "{\"message\":{\"old\":\"It's 2022! Wow!\",\"new\":\"It's 2022!\"}}",                 
    "created_at": "2022-01-05 19:05:12", 
    "updated_at": "2022-01-05 19:05:12" 
  }
]

into something like this, sorted by descending date order:

[
  {
    date: 2022-01-05,
    logs: [ array of log objects from that day ]
  },
  {
    date: 2022-01-04,
    logs: [ array of log objects from that day ]
  },
]

The code I came up with is really ugly and I could use some help, as I loop through the objects three times.

  • First, I create an object with dates as the key and arrays of logs as the value
  • Then, I convert this object into an array of objects
  • Then, I sort of the array by date.

Is there a way to build that final array by only going through the array once?

const groupLogsByDate = (logs: ActivityLog[]): { date: string, logs: ActivityLog[] }[] => {
  const groupedLogs: {[date: string]: ActivityLog[]} = {};
  Object.values(logs).forEach((log) => {
    const date: string = log.created_at.substring(0, 10);
    if (date in groupedLogs) {
      groupedLogs[date].push(log);
    } else {
      const newDate: ActivityLog[] = [];
      groupedLogs[date] = newDate;
      groupedLogs[date].push(log);
    }
  });

  const groupedArray: {date: string, logs: ActivityLog[]}[] = [];
  Object.entries(groupedLogs).forEach(([key, val]) => {
    const obj = {
      date: key,
      logs: val,
    };
    groupedArray.push(obj);
  });

  // sort the array by date string in descending order
  groupedArray.sort((a, b) => {
    if (a.date < b.date) {
      return 1;
    }
    if (b.date > a.date) {
      return -1;
    }

    return 0;
  });

  return groupedArray;
};
```
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

You have three steps, and the following suggestion brings it down to two.

You've written:

  • group logs by date
  • for each log in each log group, add a date field from the log group
  • sort all the log groups

Instead you could do:

  • group logs by date, adding a date field as you process the log
  • sort all the log groups
  const groupedLogs: {[date: string]: { date: string, logs: ActivityLog[]}} = {};
  Object.values(logs).forEach((log) => {
    const date: string = log.created_at.substring(0, 10);
    groupedLogs[date] = groupedLogs[date] || { date, logs: [] };
    groupedLogs[date].logs.push(log);
  });

  const groupedArray: {date: string, logs: ActivityLog[]}[] = Object.values(groupedLogs);

Regarding this expression: groupedLogs[date] || { date, logs: [] }

If groupedLogs[date] is truthy, then that value is used. If it is falsy, then we effectively get false || { date, logs: [] } which evaluates to { date, logs: [] }. Effectively, this means "if it already exists, use groupedLogs[date], otherwise use a new object with a value { date, logs: [] }. So if there are already logs, they're left unchanged, or if there's no value then it's initialized with an empty logs list and the date.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ This is a great, clean solution. Exactly what I was looking for. I actually didn't know you could assign using the || operator on the right-hand side. Thanks! \$\endgroup\$
    – qotsa42
    Commented Jan 12, 2022 at 19:47
1
\$\begingroup\$

The functions collect and groupBy will do this:

import { groupBy } from 'fp-ts/NonEmptyArray';
import { collect } from 'fp-ts/Record';
import { Ord } from 'fp-ts/string';

const arranged = collect(Ord)((date, logs) => ({ date, logs }))(
  groupBy(({ created_at }) => created_at.substring(0, 10))(data)
);
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.