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.

7
  • 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? Commented Apr 28, 2021 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 Commented Apr 28, 2021 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 Commented Apr 28, 2021 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 Commented Apr 28, 2021 at 17:35
  • I would recommend you to check this Q&A stackoverflow.com/questions/5525795/… It explains why the tasks you describes is not the best idea Commented Apr 28, 2021 at 18:56

3 Answers 3

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));

Sign up to request clarification or add additional context in comments.

Comments

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)

2 Comments

This sorts by the first timestamp, not the highest timestamp.
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
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; }

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.