1

I have a javascript object in a firebase realtime database that has a structure similar to the following:

const obj = {
  "quotes" : {
    "Anon" : {
      "-MZN5R9TUU5eLneHz2F_" : {
        "quote" : "I am a quote",
        "timestamp" : "1619616711347",
        "htmlId" : "id517321"
      }
    },
    "Secret Quoter" : {
      "-D9NF75TGVSeJLABz1W_" : {
        "quote" : "I am another quote",
        "timestamp" : "1690016711317",
        "htmlId" : "id519912"
      },
      "-DZNfR5THESeJLeHz1F_" : {
        "quote" : "I am a different quote",
        "timestamp" : "1349616899347",
        "htmlId" : "id515618"
      }
    },
    "General Kenobi" : {
      "-MZR666TUUGeLneHzHF_" : {
        "quote" : "Hello There",
        "timestamp" : "1919616711347",
        "htmlId" : "id511321"
      }
    }
  }
};

Now I want to reorder the object entries based on the highest timestamp attribute of each object (assuming the embedded object of each author of the quote is correctly sorted by timestamp), so the parsed response in this case would be:

{
  "quotes" : {
    "General Kenobi" : {
      "-MZR666TUUGeLneHzHF_" : {
        "quote" : "Hello There",
        "timestamp" : "1919616711347",
        "htmlId" : "id511321"
      }
    },
    "Secret Quoter" : {
      "-D9NF75TGVSeJLABz1W_" : {
        "quote" : "I am another quote",
        "timestamp" : "1690016711317",
        "htmlId" : "id519912"
      },
      "-DZNfR5THESeJLeHz1F_" : {
        "quote" : "I am a different quote",
        "timestamp" : "1349616899347",
        "htmlId" : "id515618"
      }
    },
    "Anon" : {
      "-MZN5R9TUU5eLneHz2F_" : {
        "quote" : "I am a quote",
        "timestamp" : "1619616711347",
        "htmlId" : "id517321"
      }
    }
  }
};

How would I go about doing this? There appears to be a lot of advice for sorting an array of objects, but none for actually sorting a singular object's entities by it's embedded values.


EDIT: Thanks for all the answers. To summarise, I have avoided this issue entirely by redesigning my DB schema so that quotes is an array of objects, each with a author attr. E.g.

{
  "quotes" : [
     {
        "author" : "General Kenobi",
        "quote" : "Hello There",
        "timestamp" : "1919616711347",
        "htmlId" : "id511321"
     },
     // More quotes here...
  ]

It's then possible to use the traditional method of sort() to order the list by the highest timestamp downwards. For the sake of having the question answered, I'll still accept one of the suggestions but I would recommend readers to consider if the structure of their object is correct before trying to order their object as it's a lot easier to do so with an array.

shad0w
  • 15
  • 6
  • 3
    Out of curiosity (and perhaps leading to a more meaningful answer)... Why does the order of object properties matter in this case? Where is that ordering used? – David Apr 28 '21 at 16:19
  • 1
    One thing to be aware of is that depending on the JS engine and version, the order of object properties may not be consistent or guaranteed. In JS, conventionally, if you need to specify an order for items in a collection, an array or other data structure is used – Kai Apr 28 '21 at 16:19
  • @David The order matters due to how they are being iterated through and displayed. Different parts of the angular ngFor component require different parts of this quote object to be read (there is obviously more to the firebase db than just what I am showing here). In order for the latest quotes to be displayed first, I need the quote author with the most recent quote first in the table, then the 2nd author and their quotes, and so on – shad0w Apr 28 '21 at 16:33
  • 1
    Can you please also describe how you are going to iterate through quotes after sorting? I understand what are you trying to do, but don't understand why – peter Apr 28 '21 at 17:35
  • I would recommend you to check this Q&A https://stackoverflow.com/questions/5525795/does-javascript-guarantee-object-property-order It explains why the tasks you describes is not the best idea – Yozi Apr 28 '21 at 18:56
  • @shad0w please check my solution when you have a chance. As others mentioned, insertion order can't be used for objects, but I provided a lightweight solution to sort your object exactly as you wanted as both a nested array, and a map. – Brandon McConnell Apr 28 '21 at 21:14
  • Question has been answered. Please see my update – shad0w Apr 29 '21 at 10:39

3 Answers3

1

Easiest way is to use a library like lodash to do this for you.

_.orderBy(obj.quotes, el => Object.values(el)[0].timestamp, ['desc'])

But you can also use Array Manipulation to get the desired result.

  1. Convert Object into Array
  2. Sort the array in your desired order
  3. Construct the object back
const tempArr = Object.entries(obj.quotes)

// sorts into ascending order based on a property
tempArr.sort(
    (a, b) => Object.values(a[1])[0].timestamp - Object.values(b[1])[0].timestamp
)

// since we want the descending order, reverse the array and construct the object back
const result = { quotes: Object.fromEntries(tempArr.reverse()) }
console.log(result)
boxdox
  • 552
  • 2
  • 8
  • This sorts by the first timestamp, not the highest timestamp. – 3limin4t0r Apr 28 '21 at 21:32
  • Assuming that the author timestamps are already sorted by the highest (which they are in this case), then this is correct. But is not the correct solution otherwise as @3limin4t0r said – shad0w Apr 29 '21 at 10:26
1

Sort order is not guaranteed with an object.

  1. Try reducing your dataset into a flat list first (flattened)
  2. Build a lookup map of "quoter" to "timestamp" (maxTimeMap)
  3. Sort by the lookup, followed by the timestamp in descending order for both (b - a).

const main = () => {
  const flattened = Object.entries(obj.quotes)
    .reduce((accOuter, [quoter, quotes]) =>
      Object.entries(quotes)
        .reduce((accInner, [id, { quote, timestamp, htmlId }]) =>
          [...accInner, { id, quoter, quote, timestamp, htmlId }], accOuter), [])

  const maxTimeMap = flattened.reduce((acc, { quoter, timestamp }) =>
    ({ ...acc, [quoter]: Math.max(acc[quoter] || Number.MIN_VALUE, timestamp) }), {});

  const sorted = flattened.sort((
    { timestamp: ta, quote: qa, quoter: qra },
    { timestamp: tb, quote: qb, quoter: qrb }
  ) => maxTimeMap[qrb] - maxTimeMap[qra] || tb - ta);

  console.log(sorted);
};

const obj = {
  "quotes": {
    "Anon": {
      "-MZN5R9TUU5eLneHz2F_": {
        "quote": "I am a quote",
        "timestamp": "1619616711347",
        "htmlId": "id517321"
      }
    },
    "Secret Quoter": {
      "-D9NF75TGVSeJLABz1W_": {
        "quote": "I am another quote",
        "timestamp": "1690016711317",
        "htmlId": "id519912"
      },
      "-DZNfR5THESeJLeHz1F_": {
        "quote": "I am a different quote",
        "timestamp": "1349616899347",
        "htmlId": "id515618"
      }
    },
    "General Kenobi": {
      "-MZR666TUUGeLneHzHF_": {
        "quote": "Hello There",
        "timestamp": "1919616711347",
        "htmlId": "id511321"
      }
    },
  }
};

main();
.as-console-wrapper { top: 0; max-height: 100% !important; }
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
1

As others have said, insertion order in objects is not reliably maintained. Instead, we can use an array or map for our final object. In order to sort, we'll need to reformat your object to an array of nested arrays. If you would like to use a map as the final object, we can do that as well. We'll just need to convert our sorted array first using a few array and object methods sort(), Object.entries() and map(), like this:

const obj={quotes:{Anon:{"-MZN5R9TUU5eLneHz2F_":{quote:"I am a quote",timestamp:"1619616711347",htmlId:"id517321"}},"Secret Quoter":{"-D9NF75TGVSeJLABz1W_":{quote:"I am another quote",timestamp:"1690016711317",htmlId:"id519912"},"-DZNfR5THESeJLeHz1F_":{quote:"I am a different quote",timestamp:"1349616899347",htmlId:"id515618"}},"General Kenobi":{"-MZR666TUUGeLneHzHF_":{quote:"Hello There",timestamp:"1919616711347",htmlId:"id511321"}}}};

const newObj = { quotes: Object.entries(obj.quotes).map(e => [e[0], Object.entries(e[1]).map(f => f.map((g,k,c) => k ? { id: c[k-1], ...g } : g)[1]).sort((a,b) => parseInt(b.timestamp) - parseInt(a.timestamp))]).sort((a,b) => parseInt(b[1][0].timestamp) - parseInt(a[1][0].timestamp)) };

console.log(newObj);

Because of the depth of your object, we had to perform a couple of nested map methods above. However, this produces the desired order and will retain that order reliably. All that's left now is to convert our object into an array if you'd like it to function associatively:

const obj={quotes:{Anon:{"-MZN5R9TUU5eLneHz2F_":{quote:"I am a quote",timestamp:"1619616711347",htmlId:"id517321"}},"Secret Quoter":{"-D9NF75TGVSeJLABz1W_":{quote:"I am another quote",timestamp:"1690016711317",htmlId:"id519912"},"-DZNfR5THESeJLeHz1F_":{quote:"I am a different quote",timestamp:"1349616899347",htmlId:"id515618"}},"General Kenobi":{"-MZR666TUUGeLneHzHF_":{quote:"Hello There",timestamp:"1919616711347",htmlId:"id511321"}}}};

// This is a helper function so we can console.log the Map type in the StackOverflow console. This is not needed for actual production use though.
const maplog = (_,v) => v instanceof Map ? Array.from(v.entries()) : v;

const newObj = { quotes: Object.entries(obj.quotes).map(e => [e[0], Object.entries(e[1]).map(f => f.map((g,k,c) => k ? { id: c[k-1], ...g } : g)[1]).sort((a,b) => parseInt(b.timestamp) - parseInt(a.timestamp)).reduce((a,{id,quote,timestamp,htmlId}) => (a.set(id, { quote, timestamp, htmlId })), new Map())]).sort((a,b) => parseInt(b[1].values().next().value.timestamp) - parseInt(a[1].values().next().value.timestamp)).reduce((a,c) => (a.set(c[0], c[1])), new Map()) };

console.log(JSON.stringify(newObj, maplog, 2));
Brandon McConnell
  • 5,776
  • 1
  • 20
  • 36