Hi all I have an array of objects (20 of them) that I've placed in this format to make columns in an Angular Project (note there are objects in objects). I want to sort them by object with most keys (inside each object in array), to object with least number keys inside object in array) so that when they are displayed in columns it makes the most sense.
I'm mutating a navbar variable to use for a clickable advanced search. Thanks for the help as this is my first big project as a new developer.
var clickableFilters = [
{"State": {
"0": [{value: "liquid"}, {count: 30}]
}
},
{"Country": {
"0": [{value: "USA"}, {count: 50}]
"1": [{value: "Nigeria"}, {count: 90}]
}
},
{"Color": {
"0": [{value: "blue"}, {count: 30}]
"1": [{value: "purple"}, {count: 50}]
"2": [{value: "green"}, {count: 100}]
}
}
]
How do I sort the objects by number of Keys (keys in inner object) so that it ends up (in JavaScript)
[{"Color": {}}, {"Country": {}}, {"State": {}}]
Solution 1
Using a custom comparator for Array.prototype.sort does what you need it to do
let data = [{a:1,b:2,c:3}, {a:1}, {a:1,b:2,c:3,d:4}, {a:1,b:2}]
data.sort((a,b) => Object.keys(a).length - Object.keys(b).length)
console.log(data)
Solution 2
If your data is nested, the child data should be attached to a property of a known name
let data = [
{foo: 'W', bar: {a:1,b:2,c:3}},
{foo: 'X', bar: {a:1}},
{foo: 'Y', bar: {a:1,b:2,c:3,d:4}},
{foo: 'Z', bar: {a:1,b:2}}
]
let keyToSort = "bar";
data.sort((a,b) => Object.keys(a[keyToSort]).length - Object.keys(b[keyToSort]).length)
console.log(data)
Traps and pitfalls
On the other hand, if it is guaranteed to always have exactly one key (perhaps with an unknown/dynamic name), you could write
Object.keys(a[Object.keys(a)[0]]).length
This however is obviously very ugly and error prone (what if it does have more keys - or none at all). If you have control over the data structure, you should think about refactoring it, since an Object with only one key makes not much sense - you could as well just drop one nesting level.
It's your future
You should be in the habit of battling complexity — whenever it rears its stubborn head, grasp your staff and exert an equally stubborn force back upon it.
The first solution above appears somewhat manageable, but the second one starts to get pretty thick. If you break your solution down into tiny reusable parts, you can keep complexity at bay with relative ease.
const ascComparator = (a,b) => a < b ? -1 : a > b ? 1 : 0
// use this if you want to sort data descending instead of ascending
const descComparator = (a,b) => ascComparator(b,a)
const prop = x => y => y[x]
const len = prop('length')
const comp = f => g => x => f (g (x))
const keylen = comp (len) (Object.keys)
let data = [
{foo: 'W', bar: {a:1,b:2,c:3}},
{foo: 'X', bar: {a:1}},
{foo: 'Y', bar: {a:1,b:2,c:3,d:4}},
{foo: 'Z', bar: {a:1,b:2}}
]
// same as solution #2 but much more descriptive
// "sort ascending using keylen of a.bar compared to keylen of b.bar"
data.sort((a,b) => ascComparator(keylen(a.bar), keylen(b.bar)))
console.log(data)
Breaking complexity down is a way of investing in your future. Once you wrap a bit of complexity up in its own procedure, it will always be at your disposal at a later time. Smaller procedures are also easier to test.
Each of the procedures above ascComparator, prop, len, comp, and keylen have immediately apparent intent. You can revisit these at any time and understand them with ease. And as a result of employing them, it makes your sort that much easier to read/write too.
For your data structure you can use sort() like this.
var arrayOfObjects = [
{"least#OfKeysObject": {
key1: 'value',
}
},
{"secondMost#OfKeysObj": {
key1: 'value',
key2: 'value'
}
},
{"most#OfKeysObj": {
key1: 'value',
key2: 'value',
key3: 'value'
}
}
];
var result = arrayOfObjects.sort(function(a, b) {
return Object.keys(b[Object.keys(b)[0]]).length - Object.keys(a[Object.keys(a)[0]]).length;
});
console.log(result)
Try
arrayOfObjects.sort(function(a, b) {
return Object.keys(a).length > Object.keys(b).length ? -1 : 1;
});
Related
My usage will contain 6 different object types (some which contain double nested arrays), and any possibility of number of entries, on the condition that an given entry is unique.
These objects do not have a consistent unique identifier (a unique identifier is applied in backend on submission).
here is an example of what the array may look like (only 2 object types):
arr = [
{name:"aaa",time:15},
{name:"aaa",time:22},
{timeline: "250", chars[{a},{b},{c}]},
{timeline: "220", chars[{d},{e},{f}]},
]
obj = {name:"aaa",time:22}
My intention is to gain a true or false based on if obj is inside arr
I have tried methods:
I was suggested this method & it errors: #<Object> is not a function
console.log(arr.find(obj))
I also found this suggestion but it will always return false even with the element present
console.log(arr.includes(object))
I tried this method myself, though it will always fail.
console.log(arr.filter((element, index) => element === obj)
With attempt 4, If I was to compare name, this would be insufficient as unique time would be ignored missing valid entries.
If I was to pass every field, this would also not work as each object may or may not have the field and cause error.
Its not really possible to manually pre-filter filter into distinct categories, as every time a new type is added it will need manually adding to the filter.
If there is a library which could do this that you know of, please let me know as that would be perfect. Otherwise any other suggestions (excluding separating arrays) Would be greatly appreciated.
Use arr.some() to check if the required object is present in the array.
To compare the objects, a simpler way is to Stringify both the Objects and compare them.
const arr = [
{name:"aaa",time:15},
{name:"aaa",time:22},
{name: "aaa", chars: ["a", "b", "c"]},
{name: "bbb", chars: ["d", "e", "f"]},
]
const obj1 = {name:"aaa", time: 15}
const obj2 = {name:"aaa",chars: ["a", "b", "c"]}
console.log(arr.some((element) => JSON.stringify(element) === JSON.stringify(obj1))) // true
console.log(arr.some((element) => JSON.stringify(element) === JSON.stringify(obj2))) // true
Didn't give much thought on performance.
I didn't put much thought on performace here but this might help:
function checkObjectInArray(arr, obj) {
const res = arr.some((el) => deepEqual(el, obj));
console.log(res);
}
function deepEqual(obj1, obj2) {
if (Object.keys(obj1).length !== Object.keys(obj2).length) return false;
for (let prop in obj1) {
if (!obj2.hasOwnProperty(prop) || obj2[prop] !== obj1[prop]) {
return false;
}
}
return true;
}
in your case you can use it like:
arr = [
{ name: "aaa", time: 15 },
{ name: "aaa", time: 22 },
{ timeline: "250", data: ["2", "3", "4"] },
{ timeline: "251", data: ["2", "3", "4"] }, // what is chars[{d},{e},{f}] ?!
];
obj = { name: "aaa", time: 22 };
checkObjectInArray(arr, obj);
Observation : arr is not a valid array. Nested chars is not containing a valid value.
Solution : You can simply achieve the requirement by Just converting the JSON object into a JSON string and by comparing.
This solution works fine as you are just trying to find a single object in the passed arr.
Live Demo :
const arr = [
{name:"aaa",time:15},
{name:"aaa",time:22},
{timeline: "250", chars: [{a: 1},{b: 2},{c: 3}]},
{timeline: "220", chars: [{d: 4},{e: 5},{f: 6}]},
];
const obj = {name:"aaa",time:22};
const res = JSON.stringify(arr).indexOf(JSON.stringify(obj)) !== -1 ? true : false;
console.log(res);
I have two arrays that need merging in Javascript. They are arranged as follows:
arrayA = [town1A, town2A, town3A];
arrayB = [town3B, town5B];
Each town is an object with a townName: 'town1' (matching the object variable name). Each town also has an array of occupants: [{}, {}] which each have their own personName, and a status: 'dead' or 'alive'.
My goal, is that after merging, the new array will contain every unique town according to townName (town3B and town3A both have townName : 'town3').
arrayC = [town1, town2, town3, town5]
Any new towns in arrayB (i.e., town5) should be added directly to the list. Any towns with the same name (i.e., town3) should combine their lists of occupants, but remove any "dead" people. ArrayB has priority over ArrayA when determining status, as it is "overwriting" the old data. For example:
arrayA.town3.occupants = [{name: 'Bob', status: 'alive'}, {name: 'Joe', status: 'alive'}];
arrayB.town3.occupants = [{name: 'Bob', status: 'dead'}, {name: 'Alice', status: 'alive'}];
arrayC.town3.occupants = [{name: 'Joe', status: 'alive'}, {name: 'Alice', status: 'alive'}];
I'm just struggling with the logic sequence process here and need a nudge to figure out what tools to use. Currently I'm trying to work with Lodash's _.merge and _.union in some combination. It seems I can use _.mergeWith or _.unionBy to "nest" the merging steps without resorting to manually looping over the arrays, but their usage is going over my head. If a solution exists that uses one of those, I would like to see an example to learn better how they work.
Edit: I was asked for the entire contents of an example arrayA and arrayB:
arrayA = [
{
townName: 'town1',
occupants: [
{name: 'Charlie', status: 'alive'},
{name: 'Jim', status: 'dead'}
]
},
{
townName: 'town2',
occupants: [
{name: 'Rachel', status: 'alive'},
]
},
{
townName: 'town3',
occupants: [
{name: 'Bob', status: 'alive'},
{name: 'Joe', status: 'alive'}
]
}
];
arrayB = [
{
townName: 'town3',
occupants: [
{name: 'Bob', status: 'dead'},
{name: 'Alice', status: 'alive'}
]
},
{
townName: 'town5',
occupants: [
{name: 'Sam', status: 'dead'},
{name: 'Ray', status: 'alive'},
{name: 'Bob', status: 'alive'},
]
}
];
The output I expect is:
arrayC = [
{
townName: 'town1',
occupants: [
{name: 'Charlie', status: 'alive'},
]
},
{
townName: 'town2',
occupants: [
{name: 'Rachel', status: 'alive'},
]
},
{
townName: 'town3',
occupants: [
{name: 'Joe', status: 'alive'},
{name: 'Alice', status: 'alive'}
]
},
{
townName: 'town5',
occupants: [
{name: 'Ray', status: 'alive'},
{name: 'Bob', status: 'alive'},
]
}
];
I managed to find a consistent way to do this (thanks to #Enlico for some hints). Since _.mergeWith() is recursive, you can watch for a specific nested object property and handle each property differently if needed.
// Turn each array into an Object, using "townName" as the key
var objA = _.keyBy(arrayA, 'townName');
var objB = _.keyBy(arrayB, 'townName');
// Custom handler for _.merge()
function customizer(valueA, valueB, key) {
if(key == "occupants"){
//merge occupants by 'name'. _.union prioritizes first instance (so swap A and B)
return _.unionBy(valueB, valueA, 'name');
//Else, perform normal _.merge
}
}
// Merge arrays, then turn back into array
var merged = _.values(_.mergeWith(objA, objB, customizer));
// Remove dead bodies
var filtered = _.map(merged, town => {
town.occupants = _.filter(town.occupants, person => {return person.status == "alive";});
return town;
});
The complexity with this problem is that you want to merge on 2 different layers:
you want to merge two arrays of towns, so you need to decide what to do with towns common to the two arrays;
when handling two towns with common name, you want to merge their occupants.
Now, both _.merge and _.mergeWith are good candidates to accomplish the task, except that they are for operating on objects (or associative maps, if you like), whereas you have vectors of pairs (well, not really pairs, but objects with two elements with fixed keys; name/status and townName/occupants are fundamentally key/value) at both layers mentioned above.
One function that can be useful in this case is one that turns an array of pairs into an object. Here's such a utility:
arrOfPairs2Obj = (k, v) => (arr) => _.zipObject(..._.unzip(_.map(arr, _.over([k, v]))));
Try executing the following
townArr2townMap = arrOfPairs2Obj('townName', 'occupants');
mapA = townArr2townMap(arrayA);
mapB = townArr2townMap(arrayB);
to see what it does.
Now you can merge mapA and mapB more easily…
_.mergeWith(mapA, mapB, (a, b) => {
// … well, not that easily
})
Again, a and b are arrays of "pairs" name/status, so we can reuse the abstraction I showed above, defining
personArr2personMap = arrOfPairs2Obj('name', 'status');
and using it on a and b.
But still, there are some problems. I thought that the (a, b) => { … } I wrote above would be called by _.mergeWith only for elements which have the same key across mapA and mapB, but that doesn't seem to be the case, as you can verify by running this line
_.mergeWith({a: 1, b: 3}, {b:2, c:4, d: 6}, (x, y) => [x, y])
which results in
{
a: 1
b: [3, 2]
c: [undefined, 4]
d: [undefined, 6]
}
revealing that the working lambda is called for the "clashing" keys (in the case above just b), and also for the keys which are absent in the first object (in the case above c and d), but not for those absent in the second object (in the case above a).
This is a bit unfortunate, because, while you could filter dead people out of towns which are only in arrayB, and you could also filter out those people which are dead in arrayB while alive in arrayA, you'd still have no place to filter dead people out of towns which are only in arrayA.
But let's see how far we can get. _.merge doc reads
Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources.
So we can at least handle the merging of towns common across the array in a more straightforward way. Using _.merge means that if a person is common in the two arrays, we'll always pick the one from arrayB, whether that's (still) alive or (just) dead.
Indeed, a strategy like this doesn't give you the precise solution you want, but not even one too far from it,
notSoGoodResult = _.mergeWith(mapA, mapB, (a, b) => {
return _.merge(personArr2personMap(a), personArr2personMap(b));
})
its result being the following
{
town1: [
{name: "Charlie", status: "alive"},
{name: "Jim", status: "dead"}
],
town2: [
{name: "Rachel", status: "alive"}
],
town3:
Alice: "alive",
Bob: "dead",
Joe: "alive"
},
town5: {
Bob: "alive",
Ray: "alive",
Sam: "dead"
}
}
As you can see
Bob in town3 is correctly dead,
we've not forgotten Alice in town3,
nor have we forogtten about Joe in town3.
What is left to do is
"reshaping" town3 and town5 to look like town1 and town2 (or alternatively doing the opposite),
filtering away all dead people (there's no more people appearing with both the dead and alive status, so you don't risk zombies).
Now I don't have time to finish up this, but I guess the above should help you in the right direction.
The bottom line, however, in my opinion, is that JavaScript, even with the power of Lodash, is not exactly the best tool for functional programming. _.mergeWith disappointed me, for the reason explained above.
Also, I want to mention that there a module named lodash/fp that
promotes a more functional programming (FP) friendly style by exporting an instance of lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods.
This shuould slightly help you be less verbose. With reference to your self answer, and assuming you wanted to write the lambda
person => {return person.status == "alive";}
in a more functional style, with "normal" Lodash you'd write
_.flowRight([_.curry(_.isEqual)('alive'), _.iteratee('status')])
whereas with lodash/fp you'd write
_.compose(_.isEqual('alive'), _.get('status'))
You can define a function for merging arrays with a mapper like this:
const union = (a1, a2, id, merge) => {
const dict = _.fromPairs(a1.map((v, p) => [id(v), p]))
return a2.reduce((a1, v) => {
const i = dict[id(v)]
if (i === undefined) return [...a1, v]
return Object.assign([...a1], { [i]: merge(a1[i], v) })
}, a1)
}
and use it like this:
union(
arrayA,
arrayB,
town => town.townName,
(town1, town2) => ({
...town1,
occupants: union(
town1.occupants,
town2.occupants,
occupant => occupant.name,
(occupant1, occupant2) => occupant1.status === 'alive' ? occupant1 : occupant2
).filter(occupant => occupant.status === 'alive')
})
)
I have an object that I need to filter against and return a new object. The goal is to get all ids that contain "A" in val, BUT only include ids with a unique val.
Below is what I'm currently doing, but I wonder if there's a more efficient way to do this. As can be seen when you run the code snippet, the new object should look like this:
{
"id1": {
"val": "AAA"
},
"id4": {
"val": "ABC"
}
}
const obj = {
id1: {
val: 'AAA',
},
id2: {
val: 'BBB',
},
id3: {
val: 'AAA',
},
id4: {
val: 'ABC',
},
};
// Filtered object
const obj2 = {};
let matched = '';
for (const key in obj) {
if (matched.indexOf(obj[key].val) < 0 && obj[key].val.indexOf('A') > -1) {
obj2[key] = obj[key];
matched += obj[key].val + ' ';
}
}
console.log(obj2);
Instead of building up a string for matched, you should use a Set (O(1) string comparisons for each operation instead of searching an increasingly long string in time proportional to the length of that string – and not running into issues with keys that contain spaces). includes is also a nice modern alternative to indexOf(…) > -1, although not faster.
Also, when using objects to store key/value mappings, you should use prototypeless objects – starting from Object.create(null) – to avoid setter weirdness (mostly __proto__) and tempting but fragile methods (name collisions with Object.prototype), and as a matter of good practice even when that isn’t a concern. Or just use Maps instead.
const obj = {
id1: {val: 'AAA'},
id2: {val: 'BBB'},
id3: {val: 'AAA'},
id4: {val: 'ABC'},
};
// Filtered object
const obj2 = Object.create(null);
const matched = new Set();
for (const key in obj) {
if (!matched.has(obj[key].val) && obj[key].val.includes('A')) {
obj2[key] = obj[key];
matched.add(obj[key].val);
}
}
console.log(obj2);
I have multiple json5 files that I need to join in one object.
Example the main object that other files will join to:
/equations/mass-energy-equivalence.json5
{
name: 'Mass-energy equivalence',
expression: 'E=mc^{2}',
expressionIntern: '\\mag{E}=\\mag{m}\\const{c}^{2}',
description: '...',
categories: ['physics'],
units: [
'joule'
],
constants: [
'speed-of-light'
],
magnitudes: [
'energy', 'mass'
],
values: [
{ value: 1000, units: ['joule'] }
]
}
/magnitudes/energy.json5
{ name: 'Energy', symbol: 'E', slug: 'energy', units: ['joule'], description: '', ... }
So Magnitudes have units, i have to join units/joule with magnitudes/energy and finally to the first object.
/units/joule.json5
{ name: 'Joule', symbol: 'J', slug: 'joule', description: '', ... }
And so on.
I need to join: categories, units, constants and mangitudes. Just like MySQL join. Also magnitudes has units so they have to be joined too.
So I'm trying to do a function that gets an array of nested properties like this:
This is the input to the function:
const nestedProperties = [
'categories.slug',
'constants.slug.units.slug',
'magnitudes.slug',
'units.slug',
'variables.slug.units.slug',
'values.units.slug'
];
Needs to do this for all the nestedProperties. The final object will be the output:
https://i.stack.imgur.com/yat2V.jpg
I have a function that gets the data await getData() so I need this recursive function that will set data.categories = await getData('categories', slug: 'physics')
My idea is something like this... yet not finished.
getAllData(object, nestedProperties) {
nestedProperties.forEach(async (item) => {
const parts = item.split('.');
const size = parts.length;
// We need at least two parts to get the data.
if(size === 0) console.error('Invalid Path');
// Size is even so is a multiple of 2
// ex. categories.slug
if(size % 2 === 0) {
for(let i = 0; i < size - 2; i += 2) {
// path.property -> categories.slug
if(i == 0) {
let path = parts[i];
let property = parts[i + 1];
// If the path is in the data and is an array with items
// ex. data[categories]
if(Array.isArray(data[path]) && data[path].length > 0) {
// Iterate
for(let i = 0; i < data[path].length; i++) {
// path => 'categories',
// data[path] => 'physics'
data[path] = getData(path, data[path]);
// Recursive data[path] = getAllData(path, data[path])
}
} else {
data[path] = getAllData(path, data[path])
}
}
}
} else {
// Is odd so we need to do it a bit different
// ex. 'values.units.slug'
}
}
}
Been trying a lot but not success to get to the 2 or 3 nested property :[
Thanks a lot.
I'm afraid I don't have time right now to write up a more complete explanation of this, so I'll be brief. If I find time tomorrow, I'll add more explanation. It's not complete, and doesn't handle your 'constants.slug.units.slug', so it might really be way off. (I actually simply remove the slug nodes, as I don't make sense of them.)
getData is just a dummy, meant to allow us to run something like your getData above. (Is this the equivalent of $content from your code?).
last is a trivial helper getting the last element of an array.
getPath takes a path such as ['foo', 1' 'bar'] and an object such as {foo: [{bar: 1, baz: 2}, {bar: 3, baz: 4}], qux: 5} and returns 3, the value of the bar property of the element at index 1 of the foo property of our object.
setPath simply reverses this:
setPath (['foo', 1, 'bar']) (42) ({foo: [{bar: 1, baz: 2}, {bar: 3, baz: 4}], qux: 5})
//=> {foo: [{bar: 1, baz: 2}, {bar: 42, baz: 4}], qux: 5}
fullPaths is more complex. It deals with the fact that you have fields that might be arrays or might be scalar values. It takes a path such as ['magnitudes'] and your initial data and finds the paths in the format required by getPath and setPath. Thus
fullPaths (['magnitudes']) (rawData) //=> [["magnitudes", 0], ["magnitudes", 1]]
which in turn point to 'energy' and 'mass', respectively.
With these helpers in place, we can write getAllData.
That uses fullPaths after taking your nestedProperties and turning them to arrays, removing the 'slug' substrings. With these results, we can dig into, say values.units.slug to get ['values', 0, 'units', 0], which maps to 'joules', and using 'units' and 'joules' we call getData.
After the Promises return resolve, we can fold over the results, calling such things as setPath (['values', 0, 'units', 0], promiseResult, accumulator). We return the result of that fold.
I don't know if I'll have much time to come back to this, but in case I do, I'd love to hear how close this is to your requirements. It's not clear to me for instance if you need to run those same getAllData over each result returned from getData, and if you do, whether the same nestedProperties are to be used for them.
I also don't know how to deal with the constants.slug.units.slug, as our constants are string values and don't have units.
const last = (xs) =>
xs [xs .length - 1]
const getPath = ([p, ...ps]) => (o) =>
p == undefined ? o : getPath (ps) (o && o[p])
const setPath = ([p, ...ps]) => (v) => (o) =>
p == undefined ? v : Object .assign (
Array .isArray (o) || Number .isInteger (p) ? [] : {},
{...o, [p]: setPath (ps) (v) ((o || {}) [p])}
)
const fullPaths = ([p, ...ps]) => (o) =>
p == undefined
? [[]]
: Array .isArray (o)
? o .flatMap ((x, i) => fullPaths (ps) (x [p]) .map (ns => [p, i, ...ns]))
: Object (o) === o
? p in o
? Array .isArray (o [p])
? o [p] .map ((x, i) => fullPaths (ps) (x) .flatMap ((x) => [p, i, ...x]))
: fullPaths (ps) (o [p]) .map (x => [p, ...x])
: []
: [[p]]
const getAllData = (
rawData,
nestedProperties,
paths = nestedProperties .map (s => s.split ('.'))
.map (a => a.filter (s => s !== 'slug'))
.flatMap (p => fullPaths (p) (rawData))
) =>
Promise .all (
paths .map (
p => getData (
last (p .filter (s => String (s) === s)),
getPath (p) (rawData)
)
)
) .then (res => res .reduce (
(a, r, i) => setPath (paths[i]) (r) (a),
rawData
))
const rawData = {name: 'Mass-energy equivalence', expression: 'E=mc^{2}', expressionIntern: '\\mag{E}=\\mag{m}\\const{c}^{2}', description: '...', categories: ['physics'], units: ['joule'], constants: ['speed-of-light'], magnitudes: ['energy', 'mass'], values: [{value: 1000, units: ['joule']}]}
const nestedProperties = ['categories.slug', /*'constants.slug.units.slug',*/ 'magnitudes.slug', 'units.slug', 'variables.slug.units.slug', 'values.units.slug']
getAllData (rawData, nestedProperties )
.then ((r) => console .log(JSON.stringify(r, null, 4)))
.catch (console .warn)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script> <!-- Dummy version of getData -->
const getData = ((data) => async (group, value) => group in data && value in data [group] ? Promise .resolve (data [group] [value]) : Promise .reject (`Cannot find ${group}/${value}`))({categories: {physics: {id: 2, name: "Physics", description: "Physics (from Ancient Greek: φυσική (ἐπιστήμη), romanized: physikḗ (epistḗmē), lit. 'knowledge of nature', from φύσις phýsis 'nature') is the natural science that studies matter, its motion and behavior through space and time, and the related entities of energy and force. Physics is one of the most fundamental scientific disciplines, and its main goal is to understand how the universe behaves."}, chemistry: {id: 3, name: "Chemistry", description: "Chemistry is the scientific discipline involved with elements and compounds composed of atoms, molecules and ions: their composition, structure, properties, behavior and the changes they undergo during a reaction with other substances."}}, units: {joule: {name: "Joule", symbol: {text: "J", html: "J", tex: "J"}, type: "si", categories: ["physics"], units: ["joule-per-kelvin", "joule-second"], description: "The joule (/dʒaʊl, dʒuːl/ jowl, jool) is a derived unit of energy in the International System of Units. It is equal to the energy transferred to (or work done on) an object when a force of one newton acts on that object in the direction of the force's motion through a distance of one metre (1 newton metre or $N⋅m$). It is also the energy dissipated as heat when an electric current of one ampere passes through a resistance of one ohm for one second. It is named after the English physicist James Prescott Joule (1818–1889)."}}, magnitudes: {energy: {name: 'Energy', symbol: {text: 'E', html: 'E', tex: 'E',}, categories: ['physics'], description: 'In physics, energy is the quantitative property that must be transferred to an object in order to perform work on, or to heat, the object. Energy is a conserved quantity; the law of conservation of energy states that energy can be converted in form, but not created or destroyed. The SI unit of energy is the joule, which is the energy transferred to an object by the work of moving it a distance of 1 metre against a force of 1 newton.', baseUnit: 'joule', units: ['joule']}, mass: {name: "Mass", symbol: {text: "m", html: "m", tex: "m"}, categories: ["physics"], description: "Property of matter to resist changes of the state of motion and to attract other bodies", baseUnit: "kilogram", units: ["tonne", "kilogram", "gram", "milligram", "microgram", "long-ton", "short-ton", "stone", "pound", "ounce"]}}, constants: {'speed-of-light': {name: "Speed of light in vacuum", symbol: {text: "c", html: "c", tex: "c"}, description: "The speed of light in vacuum, commonly denoted $c$, is a universal physical constant important in many areas of physics. Its exact value is defined as $299, 792, 458$ $m/s$ (approximately $300, 000$ $km/s$, or $18, 6000$ $mi/s$). It is exact because, by international agreement, a metre is defined as the length of the path travelled by light in vacuum during a time interval of $\\frac{1}{299, 792, 458}$ second. According to special relativity, $c$ is the upper limit for the speed at which conventional matter, energy or any information can travel through coordinate space.", categories: ["universal", "physics"], units: ["metre-per-second"], values: [{value: 299792458, units: "metre-per-second", exact: false, base: false}, {value: 3e8, units: "metre-per-second", exact: false}]}}})
</script>
Is there a way to make Lodash's orderBy function support accented characters?
Like á, é, ñ, etc. These are moved to the end of the array when the sort is performed.
The Problem
It sounds like it doesn't use localeCompare, defaulting instead to the equivalent of using < or >, which compares by UTF-16 code unit numeric values, not locale-aware collation (ordering).
Controlling Comparison Method
You can convert to array (if needed) and then use the native sort with localeCompare. For instance, instead of:
const result = _.orderBy(theArray, ["value"]);
you can do:
const result = theArray.slice().sort((a, b) => a.value.localeCompare(b.value));
or to sort in-place:
theArray.sort((a, b) => a.value.localeCompare(b.value));
localeCompare uses the default collation order for the default locale. Using Intl.Collator, you can have more control over the collation (like case-insensitivity, the handling of accents, the relative position of upper- and lower-case characters, etc.). For instance, if you wanted the default collation for the default locale but with upper-case characters first:
const collator = new Intl.Collator(undefined, {caseFirst: "upper"});
const result = theArray.slice().sort((a, b) => collator.compare(a.value, b.value));
Live Example:
const theArray = [
{value: "n"},
{value: "N"},
{value: "ñ"},
{value: "á"},
{value: "A"},
{value: "a"},
];
const lodashResult = _.orderBy(theArray, ["value"]);
const localeCompareResult = theArray.slice().sort((a, b) => a.value.localeCompare(b.value));
const collator = new Intl.Collator(undefined, {caseFirst: "upper"});
const collatorResult = theArray.slice().sort((a, b) => collator.compare(a.value, b.value));
show("unsorted:", theArray);
show("lodashResult:", lodashResult);
show("localeCompareResult:", localeCompareResult);
show("collatorResult:", collatorResult);
function show(label, array) {
console.log(label, "[");
for (const element of array) {
console.log(` ${JSON.stringify(element)}`);
}
console.log("]");
}
.as-console-wrapper {
max-height: 100% !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
Stable vs Unstable Sort
When I first wrote this answer, there was a slight difference between _.orderBy and the native sort: _.orderBy, like _.sortBy, always does a stable sort, whereas at the time of the original answer JavaScript's native sort was not guaranteed to be stable. Since then, though, the JavaScript specification has been modified to require a stable sort (ES2019). So both _.orderBy/_.sortBy and native sort are stable now.
If "stable" vs. "unstable" sort aren't familiar terms: A "stable" sort is one where two elements that are considered equivalent for sorting purposes are guaranteed to remain in the same position relative to each other; in an "unstable" sort, their positions relative to to each other might be swapped (which is allowed because they're "equivalent" for sorting purposes). Consider this array:
const theArray = [
{value: "x", id: 27},
{value: "z", id: 14},
{value: "x", id: 12},
];
If you do an unstable sort that sorts ascending on just value (disregarding id or any other properties the objects might have), there are two valid results:
// Valid result 1: id = 27 remained in front of id = 12
[
{value: "x", id: 27},
{value: "x", id: 12},
{value: "z", id: 14},
]
// Valid result 2: id = 27 was moved after id = 12
[
{value: "x", id: 12},
{value: "x", id: 27},
{value: "z", id: 14},
]
With a stable sort, though, only the first result is valid; the positions of equivalent elements relative to each other remains unchanged.
But again, that distinction no longer matters, since JavaScript's sort is stable now too.
I've solved it by comparing a sanitized element.
theArray.sort(function(a, b) {
return a.toLowerCase().removeAccents().localeCompare(b.toLowerCase().removeAccents());
});
The removeAccents function:
String.prototype.removeAccents = function () {
return this
.replace(/[áàãâä]/gi,"a")
.replace(/[éè¨ê]/gi,"e")
.replace(/[íìïî]/gi,"i")
.replace(/[óòöôõ]/gi,"o")
.replace(/[úùüû]/gi, "u")
.replace(/[ç]/gi, "c")
.replace(/[ñ]/gi, "n")
.replace(/[^a-zA-Z0-9]/g," ");
}