11
\$\begingroup\$

I have a JSON structure in the following example format:

[ {date: '01-01-2021', name: 'House 1', value: '500'}, {date: '01-01-2021', name: 'House 2', value: '600'}, {date: '01-01-2021', name: 'House 3', value: '700'}, {date: '01-01-2021', name: 'House 4', value: '800'}, {date: '01-02-2021', name: 'House 1', value: '200'}, {date: '01-02-2021', name: 'House 2', value: '100'}, {date: '01-02-2021', name: 'House 3', value: '300'}, {date: '01-02-2021', name: 'House 4', value: '1000'}, ] 

I have written logic to group it in the following format:

[ {date: '01-01-2021', 'House 1': 500, 'House 2': 600, 'House 3': 700, 'House 4': 800}, {date: '01-02-2021', 'House 1': 200, 'House 2': 100, 'House 3': 300, 'House 4': 1000}, ] 

Below is code I have written. It works and seems to be efficient, but I am curious if this was the best approach or if there is a simpler way to accomplish this task? The actual data set contains a total of 2274 objects that is reduced to an array of 748 objects.

Code:

const housing_table_data = ((json_data) => { const unique_dates = housing_data.reduce((accumulator, current_value) => { if (accumulator.indexOf(current_value.date) === -1) { accumulator.push(current_value.date); } return accumulator; }, []); let table_data = []; unique_dates.forEach((date, index) => { table_data.push({date: date}); let house_groupings = housing_data.filter(data => data.date=== date); house_groupings.forEach(group => { table_data[index][group.name] = group.value; }); }); return table_data; })(housing_data); 
\$\endgroup\$
2
  • 5
    \$\begingroup\$That isn't JSON. See RFC 8259, Section 7: strings use double quotes. And the only unquoted symbols in JSON are true, false and null.\$\endgroup\$
    – Kaz
    CommentedJun 12, 2021 at 7:15
  • \$\begingroup\$I wasn't aware of that, thanks for sharing! I came from a background in PL/SQL, so single quotes are very much ingrained into my programming. One day I hope to break that habit!\$\endgroup\$CommentedJun 22, 2021 at 13:44

4 Answers 4

10
\$\begingroup\$

Scoping issues

You are using an "immediately invoked function expression" (IIFE) here to scope the unique_dates, but in the process you forgot to use the function argument you introduced to correctly scope the function you have there.

Note how you have ((json_data) => { but the body of the function you define there never refers to json_data. That's not really that great.
This doesn't give you any benefit here so instead you should just unwrap that IIFE.

Naming Convention

I just want to mention that the majority of styleguides I have seen for javascript advocate the use of camelCase variable names over snake_case names. Seeing that you are consistent in your use of snake_case that's really just personal preference, though.

Algorithmic advice

You can cut the number of iterations over the json_data down to a single iteration instead of two iterations. It allows you to avoid filtering the housing_data in the forEach.

The "trick" to see is the idea of using the date as a key into an accumulator object instead of using an accumulator array. To transform the accumulator into your target structure you can then use Object.values:

const grouped_accumulated = housing_data.reduce((accumulator, current_value) => { if (!accumulator.hasOwnProperty(current_value.date)) { accumulator[current_value.date] = { date: current_value.date }; } accumulator[current_value.date][current_value.name] = current_value.value; return accumulator; }, {}); const housing_table_data = Object.values(grouped_accumulated); 
\$\endgroup\$
1
  • 1
    \$\begingroup\$Thank you so much for your response! Great catch in pointing out my scoping issue. I think I was moving to quick when refactoring and forgot to change it! Everyone at my company uses snake case even though I do prefer camel case. I figured it's easiest to just go with the flow. Your solution is very elegant and improves performance tremendously. Thank you for showing me this and also teaching me something new! I did have to modify it ever-so slightly to return the accumulator and provide a default value. Thanks again!\$\endgroup\$CommentedJun 10, 2021 at 17:56
8
\$\begingroup\$

Names!

JavaScript convention is to use camelCase for names rather than snake_case.

Using a Hash Map

Using Array.indexOf to locate unique entries has a complexity of \$O(n)\$ for each record in the source array, where \$n\$ is number of unique items. This can be reduced to \$O(1)\$ by using a hash map.

JavaScript has several ways to use hash maps (Object property names, Map, Set, WeakMap, and WeakSet)

In this case you can use Map to group by unique item values Map(key, {date: key}) creates a hash for key and stores the object {date: key} with the hash

Segregate names from data

This type of task may often require minor changes when data sources are outside your control. It pays to use a more generic solution that removes the hard coded property naming from the function.

To remove the hard coded properties date, name, value from the functions body we can divide the task into two,

  1. Create a record for each unique named property
  2. Add a new property (key/value pair) to an object based on the value of two properties in a source object.

Grouping by property

Using a Map create records for each unique named property value and add key value pairs as defined by the function above

const groupBy = (data, name, addKeyVal) => { const res = new Map(); for (const record of data) { const val = record[name]; // unique Value addKeyVal( // Add name and value to unique rec res.get(val) ?? res.set(val, {[name]: val}).get(val), // get unique rec, Create if needed record ); } return [...res.values()]; // return as array of objects } 

For more on the operator ?? (Nullish coalescing operator) used.

Adding properties

A generic function that returns a function to extract and add the named key/value pair to a existing object

const keyValToRecord = (key, val) => (dest, src) => dest[src[key]] = src[val]; 

Names defined outside function

We can now extract the data with the property names being independent of the function's code

const valueByDate = groupBy(sourceData, "date", keyValToRecord("name", "value")); 

Say your data source came with more data, eg each house had an insured value. Rather than rewrite/copy the whole function changing each named reference to value to insured you need only change the call

const insuredByDate = groupBy(sourceData, "date", keyValToRecord("name", "insured")); 

Rewrite

Putting it all together we get

const keyValToRecord = (key, val) => (dest, src) => dest[src[key]] = src[val]; const groupBy = (data, name, addKeyVal) => { const res = new Map(); for (const record of data) { const val = record[name]; addKeyVal(res.get(val) ?? res.set(val, {[name]: val}).get(val), record); } return [...res.values()]; } // Given source data const sourceData = [{date: 1, name: "House1", value: 5}, {date: 1, name: "House2", value: 6}, {date: 1, name: "House3", value: 7}, {date: 1, name: "House4", value: 8}, {date: 2, name: "House1", value: 2}, {date: 2, name: "House2", value: 1}, {date: 2, name: "House3", value: 3}, {date: 2, name: "House4", value: 1}]; // Extract the data grouped by date console.log(groupBy(sourceData, "date", keyValToRecord("name", "value")));

\$\endgroup\$
    7
    \$\begingroup\$

    In addition to what the other answers already said, I would offer

    Use a Set

    For getting a list of unique values, checking whether an item already is part of the result with a loop (and that's what indexOf/includes do) is not very efficient, leading to O(n²) time complexity. A Set uses a constant-time lookup instead, achieving overall O(n) complexity.

    Use map instead of forEach

    Your table_data is populated using a loop with a push in each iteration. And later, you look up the new item by its index to add a property for each group. Instead, use the map method and create the object at once. Or for mapping a Set to a array, you'd use Array.from.


    Combining these would lead to

    const housingTableData = (() => { const uniqueDates = new Set(housingData.map(value => value.date)); return Array.from(uniqueDates, date => { const housing = {date}; for (const group of housingData) { if (group.date === date) { housing[group.name] = group.value; } } return housing; }); })(); 

    (Personally I prefer loop constructs over filter+forEach when it comes to executing side effects, like the creation of properties on the housing object).


    And of course, as @Vogel612 advised, we can make this even more efficient by looping over the input just once. Since you tagged your question , I would however recommend to use a Map instead of an object for collecting the values by date:

    const housingTableData = (() => { const groupsByDate = new Map(); for (const {date, name, value} of housingData) { let group = groupsByDate.get(date); if (!group) { group = {date}; groupsByDate.set(date, group); } group[name] = value; } return Array.from(groupsByDate.values()); })(); 
    \$\endgroup\$
      6
      \$\begingroup\$

      I agree with the suggestions in Vogel612's answer. Below are some suggestions about the current code.

      prefer using const

      let table_data = []; 

      This variable table_data is never re-assigned so it can be declared using const. This helps avoid accidental re-assignment and other bugs.

      Use Array.includes()

      If you still needed to check if an array includes an element:

      if (accumulator.indexOf(current_value.date) === -1) { 

      This can be simplified using Array.includes()

      if (!accumulator.includes(current_value.date)) { 
      \$\endgroup\$

        Start asking to get answers

        Find the answer to your question by asking.

        Ask question

        Explore related questions

        See similar questions with these tags.