Sort objects in array with dynamic nested property keys
I'm trying to sort an array of nested objects. It's working with a static chosen key but I can't figure out how to get it dynamically.
So far I've got this code
sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
At this point the keys are hardcoded ['general']['orderID']
but I want this part to be dynamic by adding a keys
param to the sortBy
function:
sortBy = (keys, isReverse=false) => { ...
keys
is an array with the nested keys. For the above example, it will be ['general', 'fileID']
.
What are the steps that need to be taken to make this dynamic?
Note: child objects can be undefined therefore I'm using a || {}
Note 2: I'm using es6. No external packages.
javascript arrays sorting object ecmascript-6
add a comment |
I'm trying to sort an array of nested objects. It's working with a static chosen key but I can't figure out how to get it dynamically.
So far I've got this code
sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
At this point the keys are hardcoded ['general']['orderID']
but I want this part to be dynamic by adding a keys
param to the sortBy
function:
sortBy = (keys, isReverse=false) => { ...
keys
is an array with the nested keys. For the above example, it will be ['general', 'fileID']
.
What are the steps that need to be taken to make this dynamic?
Note: child objects can be undefined therefore I'm using a || {}
Note 2: I'm using es6. No external packages.
javascript arrays sorting object ecmascript-6
will it contain just two keys or that also can be dynamic
– Shubham Khatri
11 hours ago
Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic
– Thore
11 hours ago
@Thore I update my answer and find 2-liner solution - here
– Kamil Kiełczewski
2 mins ago
add a comment |
I'm trying to sort an array of nested objects. It's working with a static chosen key but I can't figure out how to get it dynamically.
So far I've got this code
sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
At this point the keys are hardcoded ['general']['orderID']
but I want this part to be dynamic by adding a keys
param to the sortBy
function:
sortBy = (keys, isReverse=false) => { ...
keys
is an array with the nested keys. For the above example, it will be ['general', 'fileID']
.
What are the steps that need to be taken to make this dynamic?
Note: child objects can be undefined therefore I'm using a || {}
Note 2: I'm using es6. No external packages.
javascript arrays sorting object ecmascript-6
I'm trying to sort an array of nested objects. It's working with a static chosen key but I can't figure out how to get it dynamically.
So far I've got this code
sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
At this point the keys are hardcoded ['general']['orderID']
but I want this part to be dynamic by adding a keys
param to the sortBy
function:
sortBy = (keys, isReverse=false) => { ...
keys
is an array with the nested keys. For the above example, it will be ['general', 'fileID']
.
What are the steps that need to be taken to make this dynamic?
Note: child objects can be undefined therefore I'm using a || {}
Note 2: I'm using es6. No external packages.
javascript arrays sorting object ecmascript-6
javascript arrays sorting object ecmascript-6
asked 11 hours ago
ThoreThore
407213
407213
will it contain just two keys or that also can be dynamic
– Shubham Khatri
11 hours ago
Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic
– Thore
11 hours ago
@Thore I update my answer and find 2-liner solution - here
– Kamil Kiełczewski
2 mins ago
add a comment |
will it contain just two keys or that also can be dynamic
– Shubham Khatri
11 hours ago
Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic
– Thore
11 hours ago
@Thore I update my answer and find 2-liner solution - here
– Kamil Kiełczewski
2 mins ago
will it contain just two keys or that also can be dynamic
– Shubham Khatri
11 hours ago
will it contain just two keys or that also can be dynamic
– Shubham Khatri
11 hours ago
Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic
– Thore
11 hours ago
Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic
– Thore
11 hours ago
@Thore I update my answer and find 2-liner solution - here
– Kamil Kiełczewski
2 mins ago
@Thore I update my answer and find 2-liner solution - here
– Kamil Kiełczewski
2 mins ago
add a comment |
7 Answers
7
active
oldest
votes
One way could be using reduce() over the new keys
argument, like this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (keys.reduce((acc, key) => (acc || {})[key], a) || '').toString();
const valueA = (keys.reduce((acc, key) => (acc || {})[key], b) || '').toString();
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
Be aware of mutable operations likesort
called insetState
which can cause bugs in your code
– user633183
2 hours ago
@user633183 I did not put new bugs, the original code already mutates thearray
. However he can clone the array withslice()
if he don't want to mutate the original one:files: prevState.files.slice().sort(...)
. Anyway, if he wants to accept your elaborated answer, for me is ok, I don't have a problem with that. But I think there is no reason to try to force him.
– Shidersz
1 hour ago
add a comment |
You can loop ovver the keys to get the values and then compare them like
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const clonedKey = [...keys];
let valueA = a;
let valueB = b
while(clonedKey.length > 0) {
const key = clonedKey.shift();
valueA = (valueA || {})[key];
valueB = (valueB || {})[key];
}
valueA = valueA || '';
valueB = valueB || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
@ziggywiggy Thanks for pointing out the error
– Shubham Khatri
10 hours ago
add a comment |
You can use a loop to extract a nested property path from an object:
const obj = {
a: {
b: {
c: 3
}
}
}
const keys = ['a', 'b', 'c']
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
console.log(`c=${value}`);
Then you can wrap the function above into a helper:
function getPath(obj, keys) {
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
return value;
}
And use it when obtaining your values:
sortBy = (isReverse = false, keys = ) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getPath(a, keys) || '';
const valueB = getPath(b, keys) || '';
// ...
})
}));
}
add a comment |
The current accepted answer, apart from putting bugs in your code is not doing much to help you. Use of a simple function deepProp
would mitigate the painful repetition -
const deepProp = (o = {}, props = ) =>
props.reduce((acc = {}, p) => acc[p], o)
Now without so much noise -
sortBy = (keys, isReverse = false) =>
this.setState ({
files: // without mutating previous state!
[...this.state.files].sort((a,b) => {
const valueA = deepProp(a, keys) || ''
const valueB = deepProp(b, keys) || ''
return isReverse
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA)
})
})
Still, this does little in terms of actually improving your program. It's riddled with complexity, and worse, this complexity will be duplicated in any component that requires similar functionality. React embraces functional style so this answer approaches the problem from a functional standpoint. In this post, we'll write sortBy
as -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
Your question poses us to learn two powerful functional concepts; we'll use these to answer the question -
- Monads
- Contravariant Functors
Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.
Your question specifically says you are not using an external packages right now, but this is a good time to reach for one. Let's take a brief look at the data.maybe
package -
A structure for values that may not be present, or computations that may fail.
Maybe(a)
explicitly models the effects that implicit inNullable
types, thus has none of the problems associated with usingnull
orundefined
— likeNullPointerException
orTypeError
.
Sounds like a good fit. We'll start by writing a function safeProp
that accepts an object and a property string as input. Intuitively, safeProp
safely returns the property p
of object o
-
const { Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
// if o is an object
Object (o) === o
// access property p on object o, wrapping the result in a Maybe
? fromNullable (o[p])
// otherwise o is not an object, return Nothing
: Nothing ()
Instead of simply returning o[p]
which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -
const generalFileId = (o = {}) =>
// access the general property
safeProp (o, 'general')
// if it exists, access the fileId property on the child
.chain (child => safeProp (child, 'fileId'))
// get the result if valid, otherwise return empty string
.getOrElse ('')
Now we have a function which can take objects of varying complexity and guarantees the result we're interested in -
console .log
( generalFileId ({ general: { fileId: 'a' } }) // 'a'
, generalFileId ({ general: { fileId: 'b' } }) // 'b'
, generalFileId ({ general: 'x' }) // ''
, generalFileId ({ a: 'x '}) // ''
, generalFileId ({ general: { err: 'x' } }) // ''
, generalFileId ({}) // ''
)
That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.
I'm intentionally avoiding showing you an implementation of Maybe
here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level.
It might help to ask how does Array work? How does it calculate or adjust the length
property when an element is added or removed from the array? How does the map
or filter
function produce a new array? If you never wondered these things before, that's okay! Array is a convenient module because it removes these concerns from the programmer's mind; it just works as advertised.
This applies regardless of whether the module is provided by JavaScript, by a third party such as from npm, or if you wrote the module yourself. If Array didn't exist, we could implement it as our own data structure with equivalent conveniences. Users of our module gain useful functionalities without introducing additional complexity. The a-ha moment comes when you realize that the programmer is his/her own user: when you run into a tough problem, write a module to free yourself from the shackles of complexity. Invent your own convenience!
We'll show a basic implementation of Maybe later in the answer, but for now we just have to finish the sort ...
We start with two basic comparators, asc
for ascending sort, and desc
for descending sort -
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
In React, we cannot mutate previous state, instead we must create new state. So to sort immutably, we must implement isort
which will not mutate the input object -
const isort = (compare = asc, xs = ) =>
xs
.slice (0) // clone
.sort (compare) // then sort
And of course a
and b
are sometimes complex objects, so case we can't directly call asc
or desc
. Below, contramap
will transform our data using one function g
, before passing the data to the other function, f
-
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId) // ascending comparator
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
Using the other comparator desc
, we can see sorting work in the other direction -
isort
( contramap (desc, generalFileId) // descending comparator
, files
)
// [ { general: { fileId: 'e' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'a' } }
// ]
Now to write the method for your React component, sortBy
. The method is essentially reduced to this.setState({ files: t (this.state.files) })
where t
is an immutable transformation of your program's state. This is good because complexity is kept out of your components where testing is difficult, and instead it resides in generic modules, which are easy to test -
sortBy = (reverse = true) =>
this.setState
( { files:
isort
( contramap
( reverse ? desc : asc
, generalFileId
)
, this.state.files
)
}
)
This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
If the nested property you need to access is not guaranteed to be general
and fileId
, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -
const deepProp = (o = {}, props = ) =>
props .reduce
( (acc, p) => // for each p, safely lookup p on child
acc .chain (child => safeProp (child, p))
, fromNullable (o) // init with Maybe o
)
const generalFileId = (o = {}) =>
deepProp (o, [ 'general', 'fileId' ]) // using deepProp
.getOrElse ('')
const fooBarQux = (o = {}) =>
deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
.getOrElse (0) // customizable default
console.log
( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
, generalFileId ({}) // ''
, fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
, fooBarQux ({ foo: { bar: 2 } }) // 0
, fooBarQux ({}) // 0
)
Above, we use the data.maybe
package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable
, Just
and Nothing
in a couple dozen lines, which we'll see later in this answer -
Run the complete demo below on repl.it
const { Just, Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const generalFileId = (o = {}) =>
safeProp (o, 'general')
.chain (child => safeProp (child, 'fileId'))
.getOrElse ('')
// ----------------------------------------------
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const isort = (compare = asc, xs = ) =>
xs
.slice (0)
.sort (compare)
// ----------------------------------------------
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId)
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
The advantages to this approach should be evident. Instead of a one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.
Lastly, sortBy
is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse
boolean; any valid comparator can be used. This means we could even write a specialized comparator that handles tie breaks using custom logic or compares year
first, then month
, then day
, etc; higher-order functions expand your possibilities tremendously.
I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe
. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way to access the module's functionalities; all other components of the module are private and are free to change or refactor as other requirements dictate -
// Maybe.js
const None =
Symbol ()
class Maybe
{ constructor (v)
{ this.value = v }
chain (f)
{ return this.value == None ? this : f (this.value) }
getOrElse (v)
{ return this.value === None ? v : this.value }
}
const Nothing = () =>
new Maybe (None)
const Just = v =>
new Maybe (v)
const fromNullable = v =>
v == null
? Nothing ()
: Just (v)
module.exports =
{ Just, Nothing, fromNullable } // note the class is hidden from the user
Then we would use it in our module. We only have to change the import (require
) but everything else just works as-is because the public API of our module matches -
const { Just, Nothing, fromNullable } =
require ('./Maybe') // this time, use our own Maybe
const safeProp = (o = {}, p = '') => // nothing changes here
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const deepProp = (o, props) => // nothing changes here
props .reduce
( (acc, p) =>
acc .chain (child => safeProp (child, p))
, fromNullable (o)
)
// ...
For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -
- multi-sort using contramap
- recursive search using contramap
add a comment |
To work with an arbitrary number of keys, you could create a function that could be reused with .reduce()
to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.
const getKey = (o, k) => (o || {})[k];
const sorter = (isReverse, ...keys) => (a, b) => {
const valueA = keys.reduce(getKey, a) || '';
const valueB = keys.reduce(getKey, b) || '';
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
};
const sortBy = (isReverse = false, ...keys) => {
this.setState(prevState => ({
files: prevState.files.sort(sorter(isReverse, ...keys))
}));
}
I also moved the sort function out to its own const
variable, and made it return a new function that uses the isReverse
value.
||
is mostly an anti-pattern in modern JavaScript thanks to default arguments -const getKey = (o = {}, k) => o[k]
– user633183
1 hour ago
add a comment |
This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare
might fail.
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getValueAtPath(a, keys);
const valueB = getValueAtPath(b, keys);
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
function getValueAtPath(file, path) {
let value = file;
let keys = [...path]; // preserve the original path array
while(value && keys.length) {
let key = keys.shift();
value = value[key];
}
return (value || '').toString();
}
add a comment |
Compare elements in sort function in following way:
let v= c => keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1) * v(a).localeCompare(v(b));
likte this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
let v=c=>keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1)*v(a).localeCompare(v(b));
})
}));
}
Here is example how this idea works:
let files = [
{ general: { fileID: "3"}},
{ general: { fileID: "1"}},
{ general: { fileID: "2"}},
{ general: { }}
];
function sortBy(keys, arr, isReverse=false) {
arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) =>
(isReverse ? -1 : 1)*v(a).localeCompare(v(b)) )
}
sortBy(['general', 'fileID'],files,true);
console.log(files);
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54735985%2fsort-objects-in-array-with-dynamic-nested-property-keys%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
7 Answers
7
active
oldest
votes
7 Answers
7
active
oldest
votes
active
oldest
votes
active
oldest
votes
One way could be using reduce() over the new keys
argument, like this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (keys.reduce((acc, key) => (acc || {})[key], a) || '').toString();
const valueA = (keys.reduce((acc, key) => (acc || {})[key], b) || '').toString();
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
Be aware of mutable operations likesort
called insetState
which can cause bugs in your code
– user633183
2 hours ago
@user633183 I did not put new bugs, the original code already mutates thearray
. However he can clone the array withslice()
if he don't want to mutate the original one:files: prevState.files.slice().sort(...)
. Anyway, if he wants to accept your elaborated answer, for me is ok, I don't have a problem with that. But I think there is no reason to try to force him.
– Shidersz
1 hour ago
add a comment |
One way could be using reduce() over the new keys
argument, like this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (keys.reduce((acc, key) => (acc || {})[key], a) || '').toString();
const valueA = (keys.reduce((acc, key) => (acc || {})[key], b) || '').toString();
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
Be aware of mutable operations likesort
called insetState
which can cause bugs in your code
– user633183
2 hours ago
@user633183 I did not put new bugs, the original code already mutates thearray
. However he can clone the array withslice()
if he don't want to mutate the original one:files: prevState.files.slice().sort(...)
. Anyway, if he wants to accept your elaborated answer, for me is ok, I don't have a problem with that. But I think there is no reason to try to force him.
– Shidersz
1 hour ago
add a comment |
One way could be using reduce() over the new keys
argument, like this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (keys.reduce((acc, key) => (acc || {})[key], a) || '').toString();
const valueA = (keys.reduce((acc, key) => (acc || {})[key], b) || '').toString();
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
One way could be using reduce() over the new keys
argument, like this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (keys.reduce((acc, key) => (acc || {})[key], a) || '').toString();
const valueA = (keys.reduce((acc, key) => (acc || {})[key], b) || '').toString();
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
edited 4 hours ago
Thore
407213
407213
answered 11 hours ago
ShiderszShidersz
7,3552831
7,3552831
Be aware of mutable operations likesort
called insetState
which can cause bugs in your code
– user633183
2 hours ago
@user633183 I did not put new bugs, the original code already mutates thearray
. However he can clone the array withslice()
if he don't want to mutate the original one:files: prevState.files.slice().sort(...)
. Anyway, if he wants to accept your elaborated answer, for me is ok, I don't have a problem with that. But I think there is no reason to try to force him.
– Shidersz
1 hour ago
add a comment |
Be aware of mutable operations likesort
called insetState
which can cause bugs in your code
– user633183
2 hours ago
@user633183 I did not put new bugs, the original code already mutates thearray
. However he can clone the array withslice()
if he don't want to mutate the original one:files: prevState.files.slice().sort(...)
. Anyway, if he wants to accept your elaborated answer, for me is ok, I don't have a problem with that. But I think there is no reason to try to force him.
– Shidersz
1 hour ago
Be aware of mutable operations like
sort
called in setState
which can cause bugs in your code– user633183
2 hours ago
Be aware of mutable operations like
sort
called in setState
which can cause bugs in your code– user633183
2 hours ago
@user633183 I did not put new bugs, the original code already mutates the
array
. However he can clone the array with slice()
if he don't want to mutate the original one: files: prevState.files.slice().sort(...)
. Anyway, if he wants to accept your elaborated answer, for me is ok, I don't have a problem with that. But I think there is no reason to try to force him.– Shidersz
1 hour ago
@user633183 I did not put new bugs, the original code already mutates the
array
. However he can clone the array with slice()
if he don't want to mutate the original one: files: prevState.files.slice().sort(...)
. Anyway, if he wants to accept your elaborated answer, for me is ok, I don't have a problem with that. But I think there is no reason to try to force him.– Shidersz
1 hour ago
add a comment |
You can loop ovver the keys to get the values and then compare them like
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const clonedKey = [...keys];
let valueA = a;
let valueB = b
while(clonedKey.length > 0) {
const key = clonedKey.shift();
valueA = (valueA || {})[key];
valueB = (valueB || {})[key];
}
valueA = valueA || '';
valueB = valueB || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
@ziggywiggy Thanks for pointing out the error
– Shubham Khatri
10 hours ago
add a comment |
You can loop ovver the keys to get the values and then compare them like
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const clonedKey = [...keys];
let valueA = a;
let valueB = b
while(clonedKey.length > 0) {
const key = clonedKey.shift();
valueA = (valueA || {})[key];
valueB = (valueB || {})[key];
}
valueA = valueA || '';
valueB = valueB || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
@ziggywiggy Thanks for pointing out the error
– Shubham Khatri
10 hours ago
add a comment |
You can loop ovver the keys to get the values and then compare them like
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const clonedKey = [...keys];
let valueA = a;
let valueB = b
while(clonedKey.length > 0) {
const key = clonedKey.shift();
valueA = (valueA || {})[key];
valueB = (valueB || {})[key];
}
valueA = valueA || '';
valueB = valueB || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
You can loop ovver the keys to get the values and then compare them like
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const clonedKey = [...keys];
let valueA = a;
let valueB = b
while(clonedKey.length > 0) {
const key = clonedKey.shift();
valueA = (valueA || {})[key];
valueB = (valueB || {})[key];
}
valueA = valueA || '';
valueB = valueB || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
edited 10 hours ago
answered 11 hours ago
Shubham KhatriShubham Khatri
85.3k15103143
85.3k15103143
@ziggywiggy Thanks for pointing out the error
– Shubham Khatri
10 hours ago
add a comment |
@ziggywiggy Thanks for pointing out the error
– Shubham Khatri
10 hours ago
@ziggywiggy Thanks for pointing out the error
– Shubham Khatri
10 hours ago
@ziggywiggy Thanks for pointing out the error
– Shubham Khatri
10 hours ago
add a comment |
You can use a loop to extract a nested property path from an object:
const obj = {
a: {
b: {
c: 3
}
}
}
const keys = ['a', 'b', 'c']
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
console.log(`c=${value}`);
Then you can wrap the function above into a helper:
function getPath(obj, keys) {
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
return value;
}
And use it when obtaining your values:
sortBy = (isReverse = false, keys = ) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getPath(a, keys) || '';
const valueB = getPath(b, keys) || '';
// ...
})
}));
}
add a comment |
You can use a loop to extract a nested property path from an object:
const obj = {
a: {
b: {
c: 3
}
}
}
const keys = ['a', 'b', 'c']
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
console.log(`c=${value}`);
Then you can wrap the function above into a helper:
function getPath(obj, keys) {
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
return value;
}
And use it when obtaining your values:
sortBy = (isReverse = false, keys = ) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getPath(a, keys) || '';
const valueB = getPath(b, keys) || '';
// ...
})
}));
}
add a comment |
You can use a loop to extract a nested property path from an object:
const obj = {
a: {
b: {
c: 3
}
}
}
const keys = ['a', 'b', 'c']
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
console.log(`c=${value}`);
Then you can wrap the function above into a helper:
function getPath(obj, keys) {
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
return value;
}
And use it when obtaining your values:
sortBy = (isReverse = false, keys = ) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getPath(a, keys) || '';
const valueB = getPath(b, keys) || '';
// ...
})
}));
}
You can use a loop to extract a nested property path from an object:
const obj = {
a: {
b: {
c: 3
}
}
}
const keys = ['a', 'b', 'c']
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
console.log(`c=${value}`);
Then you can wrap the function above into a helper:
function getPath(obj, keys) {
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
return value;
}
And use it when obtaining your values:
sortBy = (isReverse = false, keys = ) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getPath(a, keys) || '';
const valueB = getPath(b, keys) || '';
// ...
})
}));
}
const obj = {
a: {
b: {
c: 3
}
}
}
const keys = ['a', 'b', 'c']
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
console.log(`c=${value}`);
const obj = {
a: {
b: {
c: 3
}
}
}
const keys = ['a', 'b', 'c']
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
console.log(`c=${value}`);
answered 11 hours ago
nem035nem035
25.2k54062
25.2k54062
add a comment |
add a comment |
The current accepted answer, apart from putting bugs in your code is not doing much to help you. Use of a simple function deepProp
would mitigate the painful repetition -
const deepProp = (o = {}, props = ) =>
props.reduce((acc = {}, p) => acc[p], o)
Now without so much noise -
sortBy = (keys, isReverse = false) =>
this.setState ({
files: // without mutating previous state!
[...this.state.files].sort((a,b) => {
const valueA = deepProp(a, keys) || ''
const valueB = deepProp(b, keys) || ''
return isReverse
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA)
})
})
Still, this does little in terms of actually improving your program. It's riddled with complexity, and worse, this complexity will be duplicated in any component that requires similar functionality. React embraces functional style so this answer approaches the problem from a functional standpoint. In this post, we'll write sortBy
as -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
Your question poses us to learn two powerful functional concepts; we'll use these to answer the question -
- Monads
- Contravariant Functors
Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.
Your question specifically says you are not using an external packages right now, but this is a good time to reach for one. Let's take a brief look at the data.maybe
package -
A structure for values that may not be present, or computations that may fail.
Maybe(a)
explicitly models the effects that implicit inNullable
types, thus has none of the problems associated with usingnull
orundefined
— likeNullPointerException
orTypeError
.
Sounds like a good fit. We'll start by writing a function safeProp
that accepts an object and a property string as input. Intuitively, safeProp
safely returns the property p
of object o
-
const { Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
// if o is an object
Object (o) === o
// access property p on object o, wrapping the result in a Maybe
? fromNullable (o[p])
// otherwise o is not an object, return Nothing
: Nothing ()
Instead of simply returning o[p]
which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -
const generalFileId = (o = {}) =>
// access the general property
safeProp (o, 'general')
// if it exists, access the fileId property on the child
.chain (child => safeProp (child, 'fileId'))
// get the result if valid, otherwise return empty string
.getOrElse ('')
Now we have a function which can take objects of varying complexity and guarantees the result we're interested in -
console .log
( generalFileId ({ general: { fileId: 'a' } }) // 'a'
, generalFileId ({ general: { fileId: 'b' } }) // 'b'
, generalFileId ({ general: 'x' }) // ''
, generalFileId ({ a: 'x '}) // ''
, generalFileId ({ general: { err: 'x' } }) // ''
, generalFileId ({}) // ''
)
That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.
I'm intentionally avoiding showing you an implementation of Maybe
here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level.
It might help to ask how does Array work? How does it calculate or adjust the length
property when an element is added or removed from the array? How does the map
or filter
function produce a new array? If you never wondered these things before, that's okay! Array is a convenient module because it removes these concerns from the programmer's mind; it just works as advertised.
This applies regardless of whether the module is provided by JavaScript, by a third party such as from npm, or if you wrote the module yourself. If Array didn't exist, we could implement it as our own data structure with equivalent conveniences. Users of our module gain useful functionalities without introducing additional complexity. The a-ha moment comes when you realize that the programmer is his/her own user: when you run into a tough problem, write a module to free yourself from the shackles of complexity. Invent your own convenience!
We'll show a basic implementation of Maybe later in the answer, but for now we just have to finish the sort ...
We start with two basic comparators, asc
for ascending sort, and desc
for descending sort -
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
In React, we cannot mutate previous state, instead we must create new state. So to sort immutably, we must implement isort
which will not mutate the input object -
const isort = (compare = asc, xs = ) =>
xs
.slice (0) // clone
.sort (compare) // then sort
And of course a
and b
are sometimes complex objects, so case we can't directly call asc
or desc
. Below, contramap
will transform our data using one function g
, before passing the data to the other function, f
-
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId) // ascending comparator
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
Using the other comparator desc
, we can see sorting work in the other direction -
isort
( contramap (desc, generalFileId) // descending comparator
, files
)
// [ { general: { fileId: 'e' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'a' } }
// ]
Now to write the method for your React component, sortBy
. The method is essentially reduced to this.setState({ files: t (this.state.files) })
where t
is an immutable transformation of your program's state. This is good because complexity is kept out of your components where testing is difficult, and instead it resides in generic modules, which are easy to test -
sortBy = (reverse = true) =>
this.setState
( { files:
isort
( contramap
( reverse ? desc : asc
, generalFileId
)
, this.state.files
)
}
)
This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
If the nested property you need to access is not guaranteed to be general
and fileId
, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -
const deepProp = (o = {}, props = ) =>
props .reduce
( (acc, p) => // for each p, safely lookup p on child
acc .chain (child => safeProp (child, p))
, fromNullable (o) // init with Maybe o
)
const generalFileId = (o = {}) =>
deepProp (o, [ 'general', 'fileId' ]) // using deepProp
.getOrElse ('')
const fooBarQux = (o = {}) =>
deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
.getOrElse (0) // customizable default
console.log
( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
, generalFileId ({}) // ''
, fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
, fooBarQux ({ foo: { bar: 2 } }) // 0
, fooBarQux ({}) // 0
)
Above, we use the data.maybe
package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable
, Just
and Nothing
in a couple dozen lines, which we'll see later in this answer -
Run the complete demo below on repl.it
const { Just, Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const generalFileId = (o = {}) =>
safeProp (o, 'general')
.chain (child => safeProp (child, 'fileId'))
.getOrElse ('')
// ----------------------------------------------
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const isort = (compare = asc, xs = ) =>
xs
.slice (0)
.sort (compare)
// ----------------------------------------------
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId)
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
The advantages to this approach should be evident. Instead of a one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.
Lastly, sortBy
is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse
boolean; any valid comparator can be used. This means we could even write a specialized comparator that handles tie breaks using custom logic or compares year
first, then month
, then day
, etc; higher-order functions expand your possibilities tremendously.
I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe
. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way to access the module's functionalities; all other components of the module are private and are free to change or refactor as other requirements dictate -
// Maybe.js
const None =
Symbol ()
class Maybe
{ constructor (v)
{ this.value = v }
chain (f)
{ return this.value == None ? this : f (this.value) }
getOrElse (v)
{ return this.value === None ? v : this.value }
}
const Nothing = () =>
new Maybe (None)
const Just = v =>
new Maybe (v)
const fromNullable = v =>
v == null
? Nothing ()
: Just (v)
module.exports =
{ Just, Nothing, fromNullable } // note the class is hidden from the user
Then we would use it in our module. We only have to change the import (require
) but everything else just works as-is because the public API of our module matches -
const { Just, Nothing, fromNullable } =
require ('./Maybe') // this time, use our own Maybe
const safeProp = (o = {}, p = '') => // nothing changes here
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const deepProp = (o, props) => // nothing changes here
props .reduce
( (acc, p) =>
acc .chain (child => safeProp (child, p))
, fromNullable (o)
)
// ...
For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -
- multi-sort using contramap
- recursive search using contramap
add a comment |
The current accepted answer, apart from putting bugs in your code is not doing much to help you. Use of a simple function deepProp
would mitigate the painful repetition -
const deepProp = (o = {}, props = ) =>
props.reduce((acc = {}, p) => acc[p], o)
Now without so much noise -
sortBy = (keys, isReverse = false) =>
this.setState ({
files: // without mutating previous state!
[...this.state.files].sort((a,b) => {
const valueA = deepProp(a, keys) || ''
const valueB = deepProp(b, keys) || ''
return isReverse
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA)
})
})
Still, this does little in terms of actually improving your program. It's riddled with complexity, and worse, this complexity will be duplicated in any component that requires similar functionality. React embraces functional style so this answer approaches the problem from a functional standpoint. In this post, we'll write sortBy
as -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
Your question poses us to learn two powerful functional concepts; we'll use these to answer the question -
- Monads
- Contravariant Functors
Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.
Your question specifically says you are not using an external packages right now, but this is a good time to reach for one. Let's take a brief look at the data.maybe
package -
A structure for values that may not be present, or computations that may fail.
Maybe(a)
explicitly models the effects that implicit inNullable
types, thus has none of the problems associated with usingnull
orundefined
— likeNullPointerException
orTypeError
.
Sounds like a good fit. We'll start by writing a function safeProp
that accepts an object and a property string as input. Intuitively, safeProp
safely returns the property p
of object o
-
const { Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
// if o is an object
Object (o) === o
// access property p on object o, wrapping the result in a Maybe
? fromNullable (o[p])
// otherwise o is not an object, return Nothing
: Nothing ()
Instead of simply returning o[p]
which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -
const generalFileId = (o = {}) =>
// access the general property
safeProp (o, 'general')
// if it exists, access the fileId property on the child
.chain (child => safeProp (child, 'fileId'))
// get the result if valid, otherwise return empty string
.getOrElse ('')
Now we have a function which can take objects of varying complexity and guarantees the result we're interested in -
console .log
( generalFileId ({ general: { fileId: 'a' } }) // 'a'
, generalFileId ({ general: { fileId: 'b' } }) // 'b'
, generalFileId ({ general: 'x' }) // ''
, generalFileId ({ a: 'x '}) // ''
, generalFileId ({ general: { err: 'x' } }) // ''
, generalFileId ({}) // ''
)
That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.
I'm intentionally avoiding showing you an implementation of Maybe
here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level.
It might help to ask how does Array work? How does it calculate or adjust the length
property when an element is added or removed from the array? How does the map
or filter
function produce a new array? If you never wondered these things before, that's okay! Array is a convenient module because it removes these concerns from the programmer's mind; it just works as advertised.
This applies regardless of whether the module is provided by JavaScript, by a third party such as from npm, or if you wrote the module yourself. If Array didn't exist, we could implement it as our own data structure with equivalent conveniences. Users of our module gain useful functionalities without introducing additional complexity. The a-ha moment comes when you realize that the programmer is his/her own user: when you run into a tough problem, write a module to free yourself from the shackles of complexity. Invent your own convenience!
We'll show a basic implementation of Maybe later in the answer, but for now we just have to finish the sort ...
We start with two basic comparators, asc
for ascending sort, and desc
for descending sort -
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
In React, we cannot mutate previous state, instead we must create new state. So to sort immutably, we must implement isort
which will not mutate the input object -
const isort = (compare = asc, xs = ) =>
xs
.slice (0) // clone
.sort (compare) // then sort
And of course a
and b
are sometimes complex objects, so case we can't directly call asc
or desc
. Below, contramap
will transform our data using one function g
, before passing the data to the other function, f
-
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId) // ascending comparator
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
Using the other comparator desc
, we can see sorting work in the other direction -
isort
( contramap (desc, generalFileId) // descending comparator
, files
)
// [ { general: { fileId: 'e' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'a' } }
// ]
Now to write the method for your React component, sortBy
. The method is essentially reduced to this.setState({ files: t (this.state.files) })
where t
is an immutable transformation of your program's state. This is good because complexity is kept out of your components where testing is difficult, and instead it resides in generic modules, which are easy to test -
sortBy = (reverse = true) =>
this.setState
( { files:
isort
( contramap
( reverse ? desc : asc
, generalFileId
)
, this.state.files
)
}
)
This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
If the nested property you need to access is not guaranteed to be general
and fileId
, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -
const deepProp = (o = {}, props = ) =>
props .reduce
( (acc, p) => // for each p, safely lookup p on child
acc .chain (child => safeProp (child, p))
, fromNullable (o) // init with Maybe o
)
const generalFileId = (o = {}) =>
deepProp (o, [ 'general', 'fileId' ]) // using deepProp
.getOrElse ('')
const fooBarQux = (o = {}) =>
deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
.getOrElse (0) // customizable default
console.log
( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
, generalFileId ({}) // ''
, fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
, fooBarQux ({ foo: { bar: 2 } }) // 0
, fooBarQux ({}) // 0
)
Above, we use the data.maybe
package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable
, Just
and Nothing
in a couple dozen lines, which we'll see later in this answer -
Run the complete demo below on repl.it
const { Just, Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const generalFileId = (o = {}) =>
safeProp (o, 'general')
.chain (child => safeProp (child, 'fileId'))
.getOrElse ('')
// ----------------------------------------------
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const isort = (compare = asc, xs = ) =>
xs
.slice (0)
.sort (compare)
// ----------------------------------------------
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId)
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
The advantages to this approach should be evident. Instead of a one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.
Lastly, sortBy
is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse
boolean; any valid comparator can be used. This means we could even write a specialized comparator that handles tie breaks using custom logic or compares year
first, then month
, then day
, etc; higher-order functions expand your possibilities tremendously.
I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe
. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way to access the module's functionalities; all other components of the module are private and are free to change or refactor as other requirements dictate -
// Maybe.js
const None =
Symbol ()
class Maybe
{ constructor (v)
{ this.value = v }
chain (f)
{ return this.value == None ? this : f (this.value) }
getOrElse (v)
{ return this.value === None ? v : this.value }
}
const Nothing = () =>
new Maybe (None)
const Just = v =>
new Maybe (v)
const fromNullable = v =>
v == null
? Nothing ()
: Just (v)
module.exports =
{ Just, Nothing, fromNullable } // note the class is hidden from the user
Then we would use it in our module. We only have to change the import (require
) but everything else just works as-is because the public API of our module matches -
const { Just, Nothing, fromNullable } =
require ('./Maybe') // this time, use our own Maybe
const safeProp = (o = {}, p = '') => // nothing changes here
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const deepProp = (o, props) => // nothing changes here
props .reduce
( (acc, p) =>
acc .chain (child => safeProp (child, p))
, fromNullable (o)
)
// ...
For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -
- multi-sort using contramap
- recursive search using contramap
add a comment |
The current accepted answer, apart from putting bugs in your code is not doing much to help you. Use of a simple function deepProp
would mitigate the painful repetition -
const deepProp = (o = {}, props = ) =>
props.reduce((acc = {}, p) => acc[p], o)
Now without so much noise -
sortBy = (keys, isReverse = false) =>
this.setState ({
files: // without mutating previous state!
[...this.state.files].sort((a,b) => {
const valueA = deepProp(a, keys) || ''
const valueB = deepProp(b, keys) || ''
return isReverse
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA)
})
})
Still, this does little in terms of actually improving your program. It's riddled with complexity, and worse, this complexity will be duplicated in any component that requires similar functionality. React embraces functional style so this answer approaches the problem from a functional standpoint. In this post, we'll write sortBy
as -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
Your question poses us to learn two powerful functional concepts; we'll use these to answer the question -
- Monads
- Contravariant Functors
Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.
Your question specifically says you are not using an external packages right now, but this is a good time to reach for one. Let's take a brief look at the data.maybe
package -
A structure for values that may not be present, or computations that may fail.
Maybe(a)
explicitly models the effects that implicit inNullable
types, thus has none of the problems associated with usingnull
orundefined
— likeNullPointerException
orTypeError
.
Sounds like a good fit. We'll start by writing a function safeProp
that accepts an object and a property string as input. Intuitively, safeProp
safely returns the property p
of object o
-
const { Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
// if o is an object
Object (o) === o
// access property p on object o, wrapping the result in a Maybe
? fromNullable (o[p])
// otherwise o is not an object, return Nothing
: Nothing ()
Instead of simply returning o[p]
which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -
const generalFileId = (o = {}) =>
// access the general property
safeProp (o, 'general')
// if it exists, access the fileId property on the child
.chain (child => safeProp (child, 'fileId'))
// get the result if valid, otherwise return empty string
.getOrElse ('')
Now we have a function which can take objects of varying complexity and guarantees the result we're interested in -
console .log
( generalFileId ({ general: { fileId: 'a' } }) // 'a'
, generalFileId ({ general: { fileId: 'b' } }) // 'b'
, generalFileId ({ general: 'x' }) // ''
, generalFileId ({ a: 'x '}) // ''
, generalFileId ({ general: { err: 'x' } }) // ''
, generalFileId ({}) // ''
)
That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.
I'm intentionally avoiding showing you an implementation of Maybe
here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level.
It might help to ask how does Array work? How does it calculate or adjust the length
property when an element is added or removed from the array? How does the map
or filter
function produce a new array? If you never wondered these things before, that's okay! Array is a convenient module because it removes these concerns from the programmer's mind; it just works as advertised.
This applies regardless of whether the module is provided by JavaScript, by a third party such as from npm, or if you wrote the module yourself. If Array didn't exist, we could implement it as our own data structure with equivalent conveniences. Users of our module gain useful functionalities without introducing additional complexity. The a-ha moment comes when you realize that the programmer is his/her own user: when you run into a tough problem, write a module to free yourself from the shackles of complexity. Invent your own convenience!
We'll show a basic implementation of Maybe later in the answer, but for now we just have to finish the sort ...
We start with two basic comparators, asc
for ascending sort, and desc
for descending sort -
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
In React, we cannot mutate previous state, instead we must create new state. So to sort immutably, we must implement isort
which will not mutate the input object -
const isort = (compare = asc, xs = ) =>
xs
.slice (0) // clone
.sort (compare) // then sort
And of course a
and b
are sometimes complex objects, so case we can't directly call asc
or desc
. Below, contramap
will transform our data using one function g
, before passing the data to the other function, f
-
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId) // ascending comparator
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
Using the other comparator desc
, we can see sorting work in the other direction -
isort
( contramap (desc, generalFileId) // descending comparator
, files
)
// [ { general: { fileId: 'e' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'a' } }
// ]
Now to write the method for your React component, sortBy
. The method is essentially reduced to this.setState({ files: t (this.state.files) })
where t
is an immutable transformation of your program's state. This is good because complexity is kept out of your components where testing is difficult, and instead it resides in generic modules, which are easy to test -
sortBy = (reverse = true) =>
this.setState
( { files:
isort
( contramap
( reverse ? desc : asc
, generalFileId
)
, this.state.files
)
}
)
This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
If the nested property you need to access is not guaranteed to be general
and fileId
, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -
const deepProp = (o = {}, props = ) =>
props .reduce
( (acc, p) => // for each p, safely lookup p on child
acc .chain (child => safeProp (child, p))
, fromNullable (o) // init with Maybe o
)
const generalFileId = (o = {}) =>
deepProp (o, [ 'general', 'fileId' ]) // using deepProp
.getOrElse ('')
const fooBarQux = (o = {}) =>
deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
.getOrElse (0) // customizable default
console.log
( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
, generalFileId ({}) // ''
, fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
, fooBarQux ({ foo: { bar: 2 } }) // 0
, fooBarQux ({}) // 0
)
Above, we use the data.maybe
package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable
, Just
and Nothing
in a couple dozen lines, which we'll see later in this answer -
Run the complete demo below on repl.it
const { Just, Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const generalFileId = (o = {}) =>
safeProp (o, 'general')
.chain (child => safeProp (child, 'fileId'))
.getOrElse ('')
// ----------------------------------------------
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const isort = (compare = asc, xs = ) =>
xs
.slice (0)
.sort (compare)
// ----------------------------------------------
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId)
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
The advantages to this approach should be evident. Instead of a one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.
Lastly, sortBy
is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse
boolean; any valid comparator can be used. This means we could even write a specialized comparator that handles tie breaks using custom logic or compares year
first, then month
, then day
, etc; higher-order functions expand your possibilities tremendously.
I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe
. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way to access the module's functionalities; all other components of the module are private and are free to change or refactor as other requirements dictate -
// Maybe.js
const None =
Symbol ()
class Maybe
{ constructor (v)
{ this.value = v }
chain (f)
{ return this.value == None ? this : f (this.value) }
getOrElse (v)
{ return this.value === None ? v : this.value }
}
const Nothing = () =>
new Maybe (None)
const Just = v =>
new Maybe (v)
const fromNullable = v =>
v == null
? Nothing ()
: Just (v)
module.exports =
{ Just, Nothing, fromNullable } // note the class is hidden from the user
Then we would use it in our module. We only have to change the import (require
) but everything else just works as-is because the public API of our module matches -
const { Just, Nothing, fromNullable } =
require ('./Maybe') // this time, use our own Maybe
const safeProp = (o = {}, p = '') => // nothing changes here
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const deepProp = (o, props) => // nothing changes here
props .reduce
( (acc, p) =>
acc .chain (child => safeProp (child, p))
, fromNullable (o)
)
// ...
For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -
- multi-sort using contramap
- recursive search using contramap
The current accepted answer, apart from putting bugs in your code is not doing much to help you. Use of a simple function deepProp
would mitigate the painful repetition -
const deepProp = (o = {}, props = ) =>
props.reduce((acc = {}, p) => acc[p], o)
Now without so much noise -
sortBy = (keys, isReverse = false) =>
this.setState ({
files: // without mutating previous state!
[...this.state.files].sort((a,b) => {
const valueA = deepProp(a, keys) || ''
const valueB = deepProp(b, keys) || ''
return isReverse
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA)
})
})
Still, this does little in terms of actually improving your program. It's riddled with complexity, and worse, this complexity will be duplicated in any component that requires similar functionality. React embraces functional style so this answer approaches the problem from a functional standpoint. In this post, we'll write sortBy
as -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
Your question poses us to learn two powerful functional concepts; we'll use these to answer the question -
- Monads
- Contravariant Functors
Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.
Your question specifically says you are not using an external packages right now, but this is a good time to reach for one. Let's take a brief look at the data.maybe
package -
A structure for values that may not be present, or computations that may fail.
Maybe(a)
explicitly models the effects that implicit inNullable
types, thus has none of the problems associated with usingnull
orundefined
— likeNullPointerException
orTypeError
.
Sounds like a good fit. We'll start by writing a function safeProp
that accepts an object and a property string as input. Intuitively, safeProp
safely returns the property p
of object o
-
const { Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
// if o is an object
Object (o) === o
// access property p on object o, wrapping the result in a Maybe
? fromNullable (o[p])
// otherwise o is not an object, return Nothing
: Nothing ()
Instead of simply returning o[p]
which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -
const generalFileId = (o = {}) =>
// access the general property
safeProp (o, 'general')
// if it exists, access the fileId property on the child
.chain (child => safeProp (child, 'fileId'))
// get the result if valid, otherwise return empty string
.getOrElse ('')
Now we have a function which can take objects of varying complexity and guarantees the result we're interested in -
console .log
( generalFileId ({ general: { fileId: 'a' } }) // 'a'
, generalFileId ({ general: { fileId: 'b' } }) // 'b'
, generalFileId ({ general: 'x' }) // ''
, generalFileId ({ a: 'x '}) // ''
, generalFileId ({ general: { err: 'x' } }) // ''
, generalFileId ({}) // ''
)
That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.
I'm intentionally avoiding showing you an implementation of Maybe
here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level.
It might help to ask how does Array work? How does it calculate or adjust the length
property when an element is added or removed from the array? How does the map
or filter
function produce a new array? If you never wondered these things before, that's okay! Array is a convenient module because it removes these concerns from the programmer's mind; it just works as advertised.
This applies regardless of whether the module is provided by JavaScript, by a third party such as from npm, or if you wrote the module yourself. If Array didn't exist, we could implement it as our own data structure with equivalent conveniences. Users of our module gain useful functionalities without introducing additional complexity. The a-ha moment comes when you realize that the programmer is his/her own user: when you run into a tough problem, write a module to free yourself from the shackles of complexity. Invent your own convenience!
We'll show a basic implementation of Maybe later in the answer, but for now we just have to finish the sort ...
We start with two basic comparators, asc
for ascending sort, and desc
for descending sort -
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
In React, we cannot mutate previous state, instead we must create new state. So to sort immutably, we must implement isort
which will not mutate the input object -
const isort = (compare = asc, xs = ) =>
xs
.slice (0) // clone
.sort (compare) // then sort
And of course a
and b
are sometimes complex objects, so case we can't directly call asc
or desc
. Below, contramap
will transform our data using one function g
, before passing the data to the other function, f
-
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId) // ascending comparator
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
Using the other comparator desc
, we can see sorting work in the other direction -
isort
( contramap (desc, generalFileId) // descending comparator
, files
)
// [ { general: { fileId: 'e' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'a' } }
// ]
Now to write the method for your React component, sortBy
. The method is essentially reduced to this.setState({ files: t (this.state.files) })
where t
is an immutable transformation of your program's state. This is good because complexity is kept out of your components where testing is difficult, and instead it resides in generic modules, which are easy to test -
sortBy = (reverse = true) =>
this.setState
( { files:
isort
( contramap
( reverse ? desc : asc
, generalFileId
)
, this.state.files
)
}
)
This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
If the nested property you need to access is not guaranteed to be general
and fileId
, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -
const deepProp = (o = {}, props = ) =>
props .reduce
( (acc, p) => // for each p, safely lookup p on child
acc .chain (child => safeProp (child, p))
, fromNullable (o) // init with Maybe o
)
const generalFileId = (o = {}) =>
deepProp (o, [ 'general', 'fileId' ]) // using deepProp
.getOrElse ('')
const fooBarQux = (o = {}) =>
deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
.getOrElse (0) // customizable default
console.log
( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
, generalFileId ({}) // ''
, fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
, fooBarQux ({ foo: { bar: 2 } }) // 0
, fooBarQux ({}) // 0
)
Above, we use the data.maybe
package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable
, Just
and Nothing
in a couple dozen lines, which we'll see later in this answer -
Run the complete demo below on repl.it
const { Just, Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const generalFileId = (o = {}) =>
safeProp (o, 'general')
.chain (child => safeProp (child, 'fileId'))
.getOrElse ('')
// ----------------------------------------------
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const isort = (compare = asc, xs = ) =>
xs
.slice (0)
.sort (compare)
// ----------------------------------------------
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId)
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
The advantages to this approach should be evident. Instead of a one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.
Lastly, sortBy
is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse
boolean; any valid comparator can be used. This means we could even write a specialized comparator that handles tie breaks using custom logic or compares year
first, then month
, then day
, etc; higher-order functions expand your possibilities tremendously.
I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe
. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way to access the module's functionalities; all other components of the module are private and are free to change or refactor as other requirements dictate -
// Maybe.js
const None =
Symbol ()
class Maybe
{ constructor (v)
{ this.value = v }
chain (f)
{ return this.value == None ? this : f (this.value) }
getOrElse (v)
{ return this.value === None ? v : this.value }
}
const Nothing = () =>
new Maybe (None)
const Just = v =>
new Maybe (v)
const fromNullable = v =>
v == null
? Nothing ()
: Just (v)
module.exports =
{ Just, Nothing, fromNullable } // note the class is hidden from the user
Then we would use it in our module. We only have to change the import (require
) but everything else just works as-is because the public API of our module matches -
const { Just, Nothing, fromNullable } =
require ('./Maybe') // this time, use our own Maybe
const safeProp = (o = {}, p = '') => // nothing changes here
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const deepProp = (o, props) => // nothing changes here
props .reduce
( (acc, p) =>
acc .chain (child => safeProp (child, p))
, fromNullable (o)
)
// ...
For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -
- multi-sort using contramap
- recursive search using contramap
edited 1 hour ago
answered 9 hours ago
user633183user633183
70.6k21139179
70.6k21139179
add a comment |
add a comment |
To work with an arbitrary number of keys, you could create a function that could be reused with .reduce()
to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.
const getKey = (o, k) => (o || {})[k];
const sorter = (isReverse, ...keys) => (a, b) => {
const valueA = keys.reduce(getKey, a) || '';
const valueB = keys.reduce(getKey, b) || '';
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
};
const sortBy = (isReverse = false, ...keys) => {
this.setState(prevState => ({
files: prevState.files.sort(sorter(isReverse, ...keys))
}));
}
I also moved the sort function out to its own const
variable, and made it return a new function that uses the isReverse
value.
||
is mostly an anti-pattern in modern JavaScript thanks to default arguments -const getKey = (o = {}, k) => o[k]
– user633183
1 hour ago
add a comment |
To work with an arbitrary number of keys, you could create a function that could be reused with .reduce()
to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.
const getKey = (o, k) => (o || {})[k];
const sorter = (isReverse, ...keys) => (a, b) => {
const valueA = keys.reduce(getKey, a) || '';
const valueB = keys.reduce(getKey, b) || '';
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
};
const sortBy = (isReverse = false, ...keys) => {
this.setState(prevState => ({
files: prevState.files.sort(sorter(isReverse, ...keys))
}));
}
I also moved the sort function out to its own const
variable, and made it return a new function that uses the isReverse
value.
||
is mostly an anti-pattern in modern JavaScript thanks to default arguments -const getKey = (o = {}, k) => o[k]
– user633183
1 hour ago
add a comment |
To work with an arbitrary number of keys, you could create a function that could be reused with .reduce()
to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.
const getKey = (o, k) => (o || {})[k];
const sorter = (isReverse, ...keys) => (a, b) => {
const valueA = keys.reduce(getKey, a) || '';
const valueB = keys.reduce(getKey, b) || '';
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
};
const sortBy = (isReverse = false, ...keys) => {
this.setState(prevState => ({
files: prevState.files.sort(sorter(isReverse, ...keys))
}));
}
I also moved the sort function out to its own const
variable, and made it return a new function that uses the isReverse
value.
To work with an arbitrary number of keys, you could create a function that could be reused with .reduce()
to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.
const getKey = (o, k) => (o || {})[k];
const sorter = (isReverse, ...keys) => (a, b) => {
const valueA = keys.reduce(getKey, a) || '';
const valueB = keys.reduce(getKey, b) || '';
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
};
const sortBy = (isReverse = false, ...keys) => {
this.setState(prevState => ({
files: prevState.files.sort(sorter(isReverse, ...keys))
}));
}
I also moved the sort function out to its own const
variable, and made it return a new function that uses the isReverse
value.
const getKey = (o, k) => (o || {})[k];
const sorter = (isReverse, ...keys) => (a, b) => {
const valueA = keys.reduce(getKey, a) || '';
const valueB = keys.reduce(getKey, b) || '';
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
};
const sortBy = (isReverse = false, ...keys) => {
this.setState(prevState => ({
files: prevState.files.sort(sorter(isReverse, ...keys))
}));
}
const getKey = (o, k) => (o || {})[k];
const sorter = (isReverse, ...keys) => (a, b) => {
const valueA = keys.reduce(getKey, a) || '';
const valueB = keys.reduce(getKey, b) || '';
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
};
const sortBy = (isReverse = false, ...keys) => {
this.setState(prevState => ({
files: prevState.files.sort(sorter(isReverse, ...keys))
}));
}
edited 11 hours ago
answered 11 hours ago
ziggy wiggyziggy wiggy
762
762
||
is mostly an anti-pattern in modern JavaScript thanks to default arguments -const getKey = (o = {}, k) => o[k]
– user633183
1 hour ago
add a comment |
||
is mostly an anti-pattern in modern JavaScript thanks to default arguments -const getKey = (o = {}, k) => o[k]
– user633183
1 hour ago
||
is mostly an anti-pattern in modern JavaScript thanks to default arguments - const getKey = (o = {}, k) => o[k]
– user633183
1 hour ago
||
is mostly an anti-pattern in modern JavaScript thanks to default arguments - const getKey = (o = {}, k) => o[k]
– user633183
1 hour ago
add a comment |
This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare
might fail.
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getValueAtPath(a, keys);
const valueB = getValueAtPath(b, keys);
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
function getValueAtPath(file, path) {
let value = file;
let keys = [...path]; // preserve the original path array
while(value && keys.length) {
let key = keys.shift();
value = value[key];
}
return (value || '').toString();
}
add a comment |
This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare
might fail.
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getValueAtPath(a, keys);
const valueB = getValueAtPath(b, keys);
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
function getValueAtPath(file, path) {
let value = file;
let keys = [...path]; // preserve the original path array
while(value && keys.length) {
let key = keys.shift();
value = value[key];
}
return (value || '').toString();
}
add a comment |
This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare
might fail.
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getValueAtPath(a, keys);
const valueB = getValueAtPath(b, keys);
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
function getValueAtPath(file, path) {
let value = file;
let keys = [...path]; // preserve the original path array
while(value && keys.length) {
let key = keys.shift();
value = value[key];
}
return (value || '').toString();
}
This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare
might fail.
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getValueAtPath(a, keys);
const valueB = getValueAtPath(b, keys);
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
function getValueAtPath(file, path) {
let value = file;
let keys = [...path]; // preserve the original path array
while(value && keys.length) {
let key = keys.shift();
value = value[key];
}
return (value || '').toString();
}
edited 11 hours ago
answered 11 hours ago
abadalyanabadalyan
39129
39129
add a comment |
add a comment |
Compare elements in sort function in following way:
let v= c => keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1) * v(a).localeCompare(v(b));
likte this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
let v=c=>keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1)*v(a).localeCompare(v(b));
})
}));
}
Here is example how this idea works:
let files = [
{ general: { fileID: "3"}},
{ general: { fileID: "1"}},
{ general: { fileID: "2"}},
{ general: { }}
];
function sortBy(keys, arr, isReverse=false) {
arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) =>
(isReverse ? -1 : 1)*v(a).localeCompare(v(b)) )
}
sortBy(['general', 'fileID'],files,true);
console.log(files);
add a comment |
Compare elements in sort function in following way:
let v= c => keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1) * v(a).localeCompare(v(b));
likte this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
let v=c=>keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1)*v(a).localeCompare(v(b));
})
}));
}
Here is example how this idea works:
let files = [
{ general: { fileID: "3"}},
{ general: { fileID: "1"}},
{ general: { fileID: "2"}},
{ general: { }}
];
function sortBy(keys, arr, isReverse=false) {
arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) =>
(isReverse ? -1 : 1)*v(a).localeCompare(v(b)) )
}
sortBy(['general', 'fileID'],files,true);
console.log(files);
add a comment |
Compare elements in sort function in following way:
let v= c => keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1) * v(a).localeCompare(v(b));
likte this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
let v=c=>keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1)*v(a).localeCompare(v(b));
})
}));
}
Here is example how this idea works:
let files = [
{ general: { fileID: "3"}},
{ general: { fileID: "1"}},
{ general: { fileID: "2"}},
{ general: { }}
];
function sortBy(keys, arr, isReverse=false) {
arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) =>
(isReverse ? -1 : 1)*v(a).localeCompare(v(b)) )
}
sortBy(['general', 'fileID'],files,true);
console.log(files);
Compare elements in sort function in following way:
let v= c => keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1) * v(a).localeCompare(v(b));
likte this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
let v=c=>keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1)*v(a).localeCompare(v(b));
})
}));
}
Here is example how this idea works:
let files = [
{ general: { fileID: "3"}},
{ general: { fileID: "1"}},
{ general: { fileID: "2"}},
{ general: { }}
];
function sortBy(keys, arr, isReverse=false) {
arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) =>
(isReverse ? -1 : 1)*v(a).localeCompare(v(b)) )
}
sortBy(['general', 'fileID'],files,true);
console.log(files);
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
let v=c=>keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1)*v(a).localeCompare(v(b));
})
}));
}
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
let v=c=>keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1)*v(a).localeCompare(v(b));
})
}));
}
let files = [
{ general: { fileID: "3"}},
{ general: { fileID: "1"}},
{ general: { fileID: "2"}},
{ general: { }}
];
function sortBy(keys, arr, isReverse=false) {
arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) =>
(isReverse ? -1 : 1)*v(a).localeCompare(v(b)) )
}
sortBy(['general', 'fileID'],files,true);
console.log(files);
let files = [
{ general: { fileID: "3"}},
{ general: { fileID: "1"}},
{ general: { fileID: "2"}},
{ general: { }}
];
function sortBy(keys, arr, isReverse=false) {
arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) =>
(isReverse ? -1 : 1)*v(a).localeCompare(v(b)) )
}
sortBy(['general', 'fileID'],files,true);
console.log(files);
edited 3 mins ago
answered 11 hours ago
Kamil KiełczewskiKamil Kiełczewski
11.3k86694
11.3k86694
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54735985%2fsort-objects-in-array-with-dynamic-nested-property-keys%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
will it contain just two keys or that also can be dynamic
– Shubham Khatri
11 hours ago
Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic
– Thore
11 hours ago
@Thore I update my answer and find 2-liner solution - here
– Kamil Kiełczewski
2 mins ago