73

I need to flatten a nested object. Need a one liner. Not sure what the correct term for this process is. I can use pure Javascript or libraries, I particularly like underscore.

I've got ...

{
  a:2,
  b: {
    c:3
  }
}

And I want ...

{
  a:2,
  c:3
}

I've tried ...

var obj = {"fred":2,"jill":4,"obby":{"john":5}};
var resultObj = _.pick(obj, "fred")
alert(JSON.stringify(resultObj));

Which works but I also need this to work ...

var obj = {"fred":2,"jill":4,"obby":{"john":5}};
var resultObj = _.pick(obj, "john")
alert(JSON.stringify(resultObj));
danday74
  • 52,471
  • 49
  • 232
  • 283

20 Answers20

89

Here you go:

Object.assign({}, ...function _flatten(o) { return [].concat(...Object.keys(o).map(k => typeof o[k] === 'object' ? _flatten(o[k]) : ({[k]: o[k]})))}(yourObject))

Summary: recursively create an array of one-property objects, then combine them all with Object.assign.

This uses ES6 features including Object.assign or the spread operator, but it should be easy enough to rewrite not to require them.

For those who don't care about the one-line craziness and would prefer to be able to actually read it (depending on your definition of readability):

Object.assign(
  {}, 
  ...function _flatten(o) { 
    return [].concat(...Object.keys(o)
      .map(k => 
        typeof o[k] === 'object' ?
          _flatten(o[k]) : 
          ({[k]: o[k]})
      )
    );
  }(yourObject)
)
Foxhoundn
  • 1,766
  • 2
  • 13
  • 19
74

Simplified readable example, no dependencies

/**
 * Flatten a multidimensional object
 *
 * For example:
 *   flattenObject{ a: 1, b: { c: 2 } }
 * Returns:
 *   { a: 1, c: 2}
 */
export const flattenObject = (obj) => {
  const flattened = {}

  Object.keys(obj).forEach((key) => {
    const value = obj[key]

    if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
      Object.assign(flattened, flattenObject(value))
    } else {
      flattened[key] = value
    }
  })

  return flattened
}

Features

Webber
  • 4,672
  • 4
  • 29
  • 38
34

Here is a true, crazy one-liner that flats the nested object recursively:

const flatten = (obj, roots=[], sep='.') => Object.keys(obj).reduce((memo, prop) => Object.assign({}, memo, Object.prototype.toString.call(obj[prop]) === '[object Object]' ? flatten(obj[prop], roots.concat([prop]), sep) : {[roots.concat([prop]).join(sep)]: obj[prop]}), {})

Multiline version, explained:

// $roots keeps previous parent properties as they will be added as a prefix for each prop.
// $sep is just a preference if you want to seperate nested paths other than dot.
const flatten = (obj, roots = [], sep = '.') => Object
  // find props of given object
  .keys(obj)
  // return an object by iterating props
  .reduce((memo, prop) => Object.assign(
    // create a new object
    {},
    // include previously returned object
    memo,
    Object.prototype.toString.call(obj[prop]) === '[object Object]'
      // keep working if value is an object
      ? flatten(obj[prop], roots.concat([prop]), sep)
      // include current prop and value and prefix prop with the roots
      : {[roots.concat([prop]).join(sep)]: obj[prop]}
  ), {})

An example:

const obj = {a: 1, b: 'b', d: {dd: 'Y'}, e: {f: {g: 'g'}}}
const flat = flatten(obj)
{
  'a': 1, 
  'b': 'b', 
  'd.dd': 'Y', 
  'e.f.g': 'g'
}

Happy one-liner day!

muratgozel
  • 2,333
  • 27
  • 31
  • Thank you so much, I spent hours to find the way to flatten nested object. Your solution works perfectly to my case. `` {"agama_id": "1", "jenis_kelamin": "1", "nama": "Emir", "nik": "3124125251", "no_handphone": "0822305152", "pekerjaan": "Programmer", "tanggal_lahir": new Date(), "tempat_lahir": "Kediri"} `` I had this object that has nested object with a property that contains date value from Date object. Somehow it couldn't be flatten with others solutions. But your answer helps me. I'm curious why the date can't be flatten. Anyone can explain? – Galih indra Jun 03 '22 at 17:20
  • 1
    This is a really nice function and if anyone finds this and would like a more human-readable version: https://codesandbox.io/s/unruffled-engelbart-cdm1bm?file=/src/index.js – Alex McCabe Jan 09 '23 at 08:54
8

ES6 Native, Recursive:

One-liner

const crushObj = (obj) => Object.keys(obj).reduce((acc, cur) => typeof obj[cur] === 'object' ? { ...acc, ...crushObj(obj[cur]) } : { ...acc, [cur]: obj[cur] } , {})

Expanded

const crushObj = (obj = {}) => Object.keys(obj || {}).reduce((acc, cur) => {
  if (typeof obj[cur] === 'object') {
    acc = { ...acc, ...crushObj(obj[cur])}
  } else { acc[cur] = obj[cur] }
  return acc
}, {})

Usage

const obj = {
  a:2,
  b: {
    c:3
  }
}

const crushed = crushObj(obj)
console.log(crushed)
// { a: 2, c: 3 }
metaory
  • 91
  • 1
  • 3
6

My ES6 version:

const flatten = (obj) => {
    let res = {};
    for (const [key, value] of Object.entries(obj)) {
        if (typeof value === 'object') {
            res = { ...res, ...flatten(value) };
        } else {
            res[key] = value;
        }
    }
    return res;
}
Marco Lackovic
  • 6,077
  • 7
  • 55
  • 56
  • this will not keep the intermediate keys in the result so `{ a: { b: { c: { d: {` will only output `{ d: ... }` in the result instead of `{ 'a.b.c.d': ... }` – Andrew Dec 12 '22 at 08:55
5

It's not quite a one liner, but here's a solution that doesn't require anything from ES6. It uses underscore's extend method, which could be swapped out for jQuery's.

function flatten(obj) {
    var flattenedObj = {};
    Object.keys(obj).forEach(function(key){
        if (typeof obj[key] === 'object') {
            $.extend(flattenedObj, flatten(obj[key]));
        } else {
            flattenedObj[key] = obj[key];
        }
    });
    return flattenedObj;    
}
James Brierley
  • 4,630
  • 1
  • 20
  • 39
5

I like this code because it's a bit easier to understand.

Edit: I added some functionality I needed, so now it's a bit harder to understand.

const data = {
  a: "a",
  b: {
    c: "c",
    d: {
      e: "e",
      f: [
        "g",
        {
          i: "i",
          j: {},
          k: []
        }
      ]
    }
  }
};

function flatten(data, response = {}, flatKey = "", onlyLastKey = false) {
  for (const [key, value] of Object.entries(data)) {
    let newFlatKey;
    if (!isNaN(parseInt(key)) && flatKey.includes("[]")) {
      newFlatKey = (flatKey.charAt(flatKey.length - 1) == "." ? flatKey.slice(0, -1) : flatKey) + `[${key}]`;
    } else if (!flatKey.includes(".") && flatKey.length > 0) {
      newFlatKey = `${flatKey}.${key}`;
    } else {
      newFlatKey = `${flatKey}${key}`;
    }
    if (typeof value === "object" && value !== null && Object.keys(value).length > 0) {
      flatten(value, response, `${newFlatKey}.`, onlyLastKey);
    } else {
      if(onlyLastKey){
        newFlatKey = newFlatKey.split(".").pop();
      }
      if (Array.isArray(response)) {
        response.push({
          [newFlatKey.replace("[]", "")]: value
        });
      } else {
        response[newFlatKey.replace("[]", "")] = value;
      }
    }
  }
  return response;
}

console.log(flatten(data));
console.log(flatten(data, {}, "data"));
console.log(flatten(data, {}, "data[]"));
console.log(flatten(data, {}, "data", true));
console.log(flatten(data, {}, "data[]", true));
console.log(flatten(data, []));
console.log(flatten(data, [], "data"));
console.log(flatten(data, [], "data[]"));
console.log(flatten(data, [], "data", true));
console.log(flatten(data, [], "data[]", true));

Demo https://stackblitz.com/edit/typescript-flatter

For insinde a typescript class use:

function flatten(data: any, response = {}, flatKey = "", onlyLastKey = false) {
  for (const [key, value] of Object.entries(data)) {
    let newFlatKey: string;
    if (!isNaN(parseInt(key)) && flatKey.includes("[]")) {
      newFlatKey = (flatKey.charAt(flatKey.length - 1) == "." ? flatKey.slice(0, -1) : flatKey) + `[${key}]`;
    } else if (!flatKey.includes(".") && flatKey.length > 0) {
      newFlatKey = `${flatKey}.${key}`;
    } else {
      newFlatKey = `${flatKey}${key}`;
    }
    if (typeof value === "object" && value !== null && Object.keys(value).length > 0) {
      flatten(value, response, `${newFlatKey}.`, onlyLastKey);
    } else {
      if(onlyLastKey){
        newFlatKey = newFlatKey.split(".").pop();
      }
      if (Array.isArray(response)) {
        response.push({
          [newFlatKey.replace("[]", "")]: value
        });
      } else {
        response[newFlatKey.replace("[]", "")] = value;
      }
    }
  }
  return response;
}
Noob
  • 710
  • 11
  • 15
5

Here is a flatten function that correctly outputs array indexes.

function flatten(obj) {
  const result = {};
  for (const key of Object.keys(obj)) {
    if (typeof obj[key] === 'object') {
      const nested = flatten(obj[key]);
      for (const nestedKey of Object.keys(nested)) {
        result[`${key}.${nestedKey}`] = nested[nestedKey];
      }
    } else {
      result[key] = obj[key];
    }
  }
  return result;
}

Example Input:

{
  "first_name": "validations.required",
  "no_middle_name": "validations.required",
  "last_name": "validations.required",
  "dob": "validations.required",
  "citizenship": "validations.required",
  "citizenship_identity": {
    "name": "validations.required",
    "value": "validations.required"
  },
  "address": [
    {
      "country_code": "validations.required",
      "street": "validations.required",
      "city": "validations.required",
      "state": "validations.required",
      "zipcode": "validations.required",
      "start_date": "validations.required",
      "end_date": "validations.required"
    },
    {
      "country_code": "validations.required",
      "street": "validations.required",
      "city": "validations.required",
      "state": "validations.required",
      "zipcode": "validations.required",
      "start_date": "validations.required",
      "end_date": "validations.required"
    }
  ]
}

Example Output:

const flattenedOutput = flatten(inputObj);
{
  "first_name": "validations.required",
  "no_middle_name": "validations.required",
  "last_name": "validations.required",
  "dob": "validations.required",
  "citizenship": "validations.required",
  "citizenship_identity.name": "validations.required",
  "citizenship_identity.value": "validations.required",
  "address.0.country_code": "validations.required",
  "address.0.street": "validations.required",
  "address.0.city": "validations.required",
  "address.0.state": "validations.required",
  "address.0.zipcode": "validations.required",
  "address.0.start_date": "validations.required",
  "address.0.end_date": "validations.required",
  "address.1.country_code": "validations.required",
  "address.1.street": "validations.required",
  "address.1.city": "validations.required",
  "address.1.state": "validations.required",
  "address.1.zipcode": "validations.required",
  "address.1.start_date": "validations.required",
  "address.1.end_date": "validations.required"
}
Andrew
  • 3,733
  • 1
  • 35
  • 36
4

Here's an ES6 version in TypeScript. It takes the best of answers given here and elsewhere. Some features:

  • Supports Date objects and converts them into ISO strings
  • Puts an underscore between the parent's and child's key (e.g. {a: {b: 'test'}} becomes {a_b: 'test'}
const flatten = (obj: Record<string, unknown>, parent?: string): Record<string, unknown> => {
    let res: Record<string, unknown> = {}

    for (const [key, value] of Object.entries(obj)) {
        const propName = parent ? parent + '_' + key : key
        const flattened: Record<string, unknown> = {}

        if (value instanceof Date) {
            flattened[key] = value.toISOString()
        } else if(typeof value === 'object' && value !== null){
            res = {...res, ...flatten(value as Record<string, unknown>, propName)}
        } else {
            res[propName] = value
        }
    }

    return res
}

An example:

const example = {
    person: {
        firstName: 'Demo',
        lastName: 'Person'
    },
    date: new Date(),
    hello: 'world'
}

// becomes

const flattenedExample = {
    person_firstName: 'Demo',
    person_lastName: 'Person',
    date: '2021-10-18T10:41:14.278Z',
    hello: 'world'
}
Dennis Ameling
  • 707
  • 7
  • 15
3

This is a function I've got in my common libraries for exactly this purpose. I believe I got this from a similar stackoverflow question, but cannot remember which (edit: Fastest way to flatten / un-flatten nested JSON objects - Thanks Yoshi!)

function flatten(data) {
    var result = {};
    function recurse (cur, prop) {
        if (Object(cur) !== cur) {
            result[prop] = cur;
        } else if (Array.isArray(cur)) {
             for(var i=0, l=cur.length; i<l; i++)
                 recurse(cur[i], prop + "[" + i + "]");
            if (l == 0)
                result[prop] = [];
        } else {
            var isEmpty = true;
            for (var p in cur) {
                isEmpty = false;
                recurse(cur[p], prop ? prop+"."+p : p);
            }
            if (isEmpty && prop)
                result[prop] = {};
        }
    }
    recurse(data, "");
    return result;
}

This can then be called as follows:

var myJSON = '{a:2, b:{c:3}}';
var myFlattenedJSON = flatten(myJSON);

You can also append this function to the standard Javascript string class as follows:

String.prototype.flattenJSON = function() {
    var data = this;
    var result = {};
    function recurse (cur, prop) {
        if (Object(cur) !== cur) {
            result[prop] = cur;
        } else if (Array.isArray(cur)) {
             for(var i=0, l=cur.length; i<l; i++)
                 recurse(cur[i], prop + "[" + i + "]");
            if (l == 0)
                result[prop] = [];
        } else {
            var isEmpty = true;
            for (var p in cur) {
                isEmpty = false;
                recurse(cur[p], prop ? prop+"."+p : p);
            }
            if (isEmpty && prop)
                result[prop] = {};
        }
    }
    recurse(data, "");
    return result;
}

With which, you can do the following:

var flattenedJSON = '{a:2, b:{c:3}}'.flattenJSON();
Community
  • 1
  • 1
Sk93
  • 3,676
  • 3
  • 37
  • 67
3

Here are vanilla solutions that work for arrays, primitives, regular expressions, functions, any number of nested object levels, and just about everything else I could throw at them. The first overwrites property values in the manner that you would expect from Object.assign.

((o) => {
  return o !== Object(o) || Array.isArray(o) ? {}
    : Object.assign({}, ...function leaves(o) {
    return [].concat.apply([], Object.entries(o)
      .map(([k, v]) => {
        return (( !v || typeof v !== 'object'
            || !Object.keys(v).some(key => v.hasOwnProperty(key))
            || Array.isArray(v))
          ? {[k]: v}
          : leaves(v)
        );
      })
    );
  }(o))
})(o)

The second accumulates values into an array.

((o) => {
  return o !== Object(o) || Array.isArray(o) ? {}
    : (function () {
      return Object.values((function leaves(o) {
        return [].concat.apply([], !o ? [] : Object.entries(o)
          .map(([k, v]) => {
            return (( !v || typeof v !== 'object'
                || !Object.keys(v).some(k => v.hasOwnProperty(k))
                || (Array.isArray(v) && !v.some(el => typeof el === 'object')))
              ? {[k]: v}
              : leaves(v)
            );
          })
        );
      }(o))).reduce((acc, cur) => {
        return ((key) => {
          acc[key] = !acc[key] ? [cur[key]]
            : new Array(...new Set(acc[key].concat([cur[key]])))
        })(Object.keys(cur)[0]) ? acc : acc
      }, {})
    })(o);
})(o)

Also please do not include code like this in production as it is terribly difficult to debug.

function leaves1(o) {
  return ((o) => {
    return o !== Object(o) || Array.isArray(o) ? {}
      : Object.assign({}, ...function leaves(o) {
      return [].concat.apply([], Object.entries(o)
        .map(([k, v]) => {
          return (( !v || typeof v !== 'object'
              || !Object.keys(v).some(key => v.hasOwnProperty(key))
              || Array.isArray(v))
            ? {[k]: v}
            : leaves(v)
          );
        })
      );
    }(o))
  })(o);
}

function leaves2(o) {
  return ((o) => {
    return o !== Object(o) || Array.isArray(o) ? {}
      : (function () {
        return Object.values((function leaves(o) {
          return [].concat.apply([], !o ? [] : Object.entries(o)
            .map(([k, v]) => {
              return (( !v || typeof v !== 'object'
                  || !Object.keys(v).some(k => v.hasOwnProperty(k))
                  || (Array.isArray(v) && !v.some(el => typeof el === 'object')))
                ? {[k]: v}
                : leaves(v)
              );
            })
          );
        }(o))).reduce((acc, cur) => {
          return ((key) => {
            acc[key] = !acc[key] ? [cur[key]]
              : new Array(...new Set(acc[key].concat([cur[key]])))
          })(Object.keys(cur)[0]) ? acc : acc
        }, {})
      })(o);
  })(o);
}

const obj = {
  l1k0: 'foo',
  l1k1: {
    l2k0: 'bar',
    l2k1: {
      l3k0: {},
      l3k1: null
    },
    l2k2: undefined
  },
  l1k2: 0,
  l2k3: {
    l3k2: true,
    l3k3: {
      l4k0: [1,2,3],
      l4k1: [4,5,'six', {7: 'eight'}],
      l4k2: {
        null: 'test',
        [{}]: 'obj',
        [Array.prototype.map]: Array.prototype.map,
        l5k3: ((o) => (typeof o === 'object'))(this.obj),
      }
    }
  },
  l1k4: '',
  l1k5: new RegExp(/[\s\t]+/g),
  l1k6: function(o) { return o.reduce((a,b) => a+b)},
  false: [],
}
const objs = [null, undefined, {}, [], ['non', 'empty'], 42, /[\s\t]+/g, obj];

objs.forEach(o => {
  console.log(leaves1(o));
});
objs.forEach(o => {
  console.log(leaves2(o));
});
pkfm
  • 451
  • 3
  • 7
3

Here is an actual oneliner of just 91 characters, using Underscore. (Of course. What else?)

var { reduce, isObject } = _;

var data = {
    a: 1,
    b: 2,
    c: {
        d: 3,
        e: 4,
        f: {
            g: 5
        },
        h: 6
    }
};

var tip = (v, m={}) => reduce(v, (m, v, k) => isObject(v) ? tip(v, m) : {...m, [k]: v}, m);

console.log(tip(data));
<script src="https://underscorejs.org/underscore-umd-min.js"></script>

Readable version:

var { reduce, isObject, extend } = _;

var data = {
    a: 1,
    b: 2,
    c: {
        d: 3,
        e: 4,
        f: {
            g: 5
        },
        h: 6
    }
};

// This function is passed to _.reduce below.
// We visit a single key of the input object. If the value
// itself is an object, we recursively copy its keys into
// the output object (memo) by calling tip. Otherwise we
// add the key-value pair to the output object directly.
function tipIteratee(memo, value, key) {
    if (isObject(value)) return tip(value, memo);
    return extend(memo, {[key]: value});
}

// The entry point of the algorithm. Walks over the keys of
// an object using _.reduce, collecting all tip keys in memo.
function tip(value, memo = {}) {
    return _.reduce(value, tipIteratee, memo);
}

console.log(tip(data));
<script src="https://underscorejs.org/underscore-umd-min.js"></script>

Also works with Lodash.

Julian
  • 4,176
  • 19
  • 40
2

To flatten only the first level of the object and merge duplicate object keys into an array:

var myObj = {
  id: '123',
  props: {
    Name: 'Apple',
    Type: 'Fruit',
    Link: 'apple.com',
    id: '345'
  },
  moreprops: {
    id: "466"
  }
};

const flattenObject = (obj) => {
  let flat = {};
  for (const [key, value] of Object.entries(obj)) {
    if (typeof value === 'object' && value !== null) {
      for (const [subkey, subvalue] of Object.entries(value)) {
        // avoid overwriting duplicate keys: merge instead into array
        typeof flat[subkey] === 'undefined' ?
          flat[subkey] = subvalue :
          Array.isArray(flat[subkey]) ?
            flat[subkey].push(subvalue) :
            flat[subkey] = [flat[subkey], subvalue]
      }
    } else {
      flat = {...flat, ...{[key]: value}};
    }
  }
  return flat;
}

console.log(flattenObject(myObj))
Johannes
  • 316
  • 3
  • 4
2

Object.assign requires a polyfill. This version is similar to previous ones, but it is not using Object.assign and it is still keep tracking of parent's name

const flatten = (obj, parent = null) => Object.keys(obj).reduce((acc, cur) => 
    typeof obj[cur] === 'object' ? { ...acc, ...flatten(obj[cur], cur) } :
    { ...acc, [((parent) ? parent + '.' : "") + cur]: obj[cur] } , {})

const obj = {
  a:2,
  b: {
    c:3
  }
}

const flattened = flatten(obj)
console.log(flattened)
  • this will not keep the root keys in the result so `{ a: [ { b, c } ] }` will only output `{ 0.b: ..., 0.c: ... }` in the result instead of `{ 'a.0.b': ..., 'a.0.c': ... }` – Andrew Dec 12 '22 at 09:17
1
function flatten(obj: any) {
  return Object.keys(obj).reduce((acc, current) => {
    const key = `${current}`;
    const currentValue = obj[current];
    if (Array.isArray(currentValue) || Object(currentValue) === currentValue) {
      Object.assign(acc, flatten(currentValue));
    } else {
      acc[key] = currentValue;
    }
    return acc;
  }, {});
};

let obj = {
  a:2,
  b: {
    c:3
  }
}

console.log(flatten(obj))

Demo https://stackblitz.com/edit/typescript-flatten-json

Hau Le
  • 121
  • 1
  • 5
1

Here goes, not thoroughly tested. Utilizes ES6 syntax too!!

loopValues(val){
let vals = Object.values(val);
let q = [];
vals.forEach(elm => {
  if(elm === null || elm === undefined) { return; }
    if (typeof elm === 'object') {
      q = [...q, ...this.loopValues(elm)];
    }
    return q.push(elm);
  });
  return q;
}

let flatValues = this.loopValues(object)
flatValues = flatValues.filter(elm => typeof elm !== 'object');
console.log(flatValues);
1

I know its been very long, but it may be helpful for some one in the future

I've used recursion

let resObj = {};
function flattenObj(obj) {
    for (let key in obj) {
        if (!(typeof obj[key] == 'object')) {
            // console.log('not an object', key);
            resObj[key] = obj[key];
            // console.log('res obj is ', resObj);
        } else {
            flattenObj(obj[key]);
        }
    }

    return resObj;
}
dinesh dsv
  • 13
  • 1
  • 4
1

Here's my TypeScript extension from @Webber's answer. Also supports dates:

private flattenObject(obj: any): any {
  const flattened = {};

  for (const key of Object.keys(obj)) {
    if (isNullOrUndefined(obj[key])) {
      continue;
    }

    if (typeof obj[key].getMonth === 'function') {
      flattened[key] = (obj[key] as Date).toISOString();
    } else if (typeof obj[key] === 'object' && obj[key] !== null) {
      Object.assign(flattened, this.flattenObject(obj[key]));
    } else {
      flattened[key] = obj[key];
    }
  }

  return flattened;
}
Web Dev
  • 2,677
  • 2
  • 30
  • 41
1

here is a simple typescript function which flatten object and concatenates keys:

function crushObj(
    rootObj:{[key: string]: any},
    obj: any,
    split = '/',
    prefix = ''
) {
    if (typeof obj === 'object') {
        for (const key of Object.keys(obj)) {
            const val = obj[key];
            delete obj[key];

            const rootKey = prefix.length > 0 ? `${prefix}${split}${key}` : key;

            crushObj(rootObj, val, split, rootKey)
        }
    }
    else {
        rootObj[prefix] = obj;
    }
}

You can use it like :

const obj = {
    name: 'John',
    address: {
      street: 'Aldo',
      number: 12,
    }
}
crushObj(obj, obj);

Results:

{
    name: "John",
    "address/street": "Aldo",
    "address/number": 12
}
Fabien
  • 21
  • 2
0
const obj = {
  a:2,
  b: {
    c:3
  }
}
// recursive function for extracting keys
function extractKeys(obj) {
  let flattenedObj = {};
  for(let [key, value] of Object.entries(obj)){
    if(typeof value === "object") {
      flattenedObj =  {...flattenedObj, ...extractKeys(value)};
    } else {
      flattenedObj[key] = value;
    }
  }
  return flattenedObj;
}
 
//  main code
let flattenedObj = extractKeys(obj);
console.log(flattenedObj);
  • This is the same as [another answer](https://stackoverflow.com/a/68047417/6036546) on this question by Marco Lackovic. – Besworks Apr 04 '22 at 23:54