2

I'm trying to sort an array of objects that has a nullable boolean value and a title. If an item in the list has been set as important the "ReadUnderstood" is either true or false, and if not, it's null. I want the list to be ordered alphabetically if "ReadUnderstood" is true or null, but if the value is false, I want it to the top of the list.

The closest to what I want I got with the code below. This returns the list in alphabetical order with the items where "ReadUnderstood" is false at the top of the list. But the items where "ReadUnderstood" is true ends up at the end of the list instead of in the alphabetiacal order. Any help would be greatly appreciated.

items = [
    {Title: 'A', ReadUnderstood: null},
    {Title: 'C', ReadUnderstood: false},
    {Title: 'E', ReadUnderstood: null},
    {Title: 'B', ReadUnderstood: true},
    {Title: 'D', ReadUnderstood: true},
    {Title: 'F', ReadUnderstood: null},
]

items.sort((a, b) => { 
    return (b.ReadUnderstood != null && b.ReadUnderstood == false) - (a.ReadUnderstood != null && a.ReadUnderstood == false) || a.Title - b.Title; 
})

Desired result:
items = [
    {Title: 'C', ReadUnderstood: false},
    {Title: 'A', ReadUnderstood: null},
    {Title: 'B', ReadUnderstood: true},
    {Title: 'D', ReadUnderstood: true},
    {Title: 'E', ReadUnderstood: null},
    {Title: 'F', ReadUnderstood: null},
]
3
  • Please add a sample of input and the expected output to create a minimal reproducible example Commented Aug 16, 2019 at 11:16
  • What about multiple false values. Do they need to be sorted alphabetically as well? Commented Aug 16, 2019 at 11:29
  • That would be preferable as well, yes :) Commented Aug 16, 2019 at 11:30

4 Answers 4

3

You are close. The compareFunction should return a number and based on whether it is positive, negative or zero, the two items (a, b) being compared are moved relative to each other. Subtracting booleans returns a number. So, the first condition works fine. For strings, you need to use localeCompare to sort them alphabetically.

Also, you can simplify the first condition. You don't need to check for null and make a strict equality check for false.

items.sort((a, b) => 
  (b.ReadUnderstood === false) - (a.ReadUnderstood === false) 
    || a.Title.localeCompare(b.Title)
)

Here's a working snippet:

const items = [
    {Title: 'A', ReadUnderstood: null},
    {Title: 'C', ReadUnderstood: false},
    {Title: 'E', ReadUnderstood: null},
    {Title: 'B', ReadUnderstood: true},
    {Title: 'D', ReadUnderstood: true},
    {Title: 'F', ReadUnderstood: null},
]

items.sort((a, b) => 
  (b.ReadUnderstood === false) - (a.ReadUnderstood === false) 
    || a.Title.localeCompare(b.Title)
)

console.log(items)

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

7 Comments

Weird. Your snippet is doing what I want, but when I tried to implement it in my code the list was only sorted alphabetically
@Hammis82 I'm using an implicit return in arrow functios. Since you have {} in your compareFunction, you need to add return
I didn't know a boolean could be subtracted/added to another boolean. Learning something every day. For others reading: true represents 1 and false represents 0. true - false //=> 1
@3limin4t0r to expand on "true represents 1": you can subtract anything in javascript. They are coerced to differenct types based on the operands. In arithmetic context, ToNumber is called on the 2 operands. From the linked table, you can see, "The result is 1 if the argument is true. The result is +0 if the argument is false".
@3limin4t0r For objects, ToNumber calls, valueOf method. So if you override the valueOf function of objects, you can use them in arithmetic operations. var a = { valueOf: () => 1 }; var b = { valueOf: () => 2 } So, b - a returns 1
|
1

You can't subtract strings to compare them (subtracting non-number strings will always result in NaN).
You can only use - for sorting when the involved operands are numeric (numbers, booleans, etc...), otherwise you'll end up with a wrong sort order.

To compare strings you could either use > and <:

if(a.Text < b.Text)
  return -1;
else if(a.Text > b.Text)
  return 1;
else
  return 0;

Or use the String.prototype.localeCompare method:

return a.Text.localeCompare(b.Text);

localeCompare() also has the advantage of correctly sorting unicode characters (e.g. é).

Full working example with your items:

let items = [
  {Title: 'A', ReadUnderstood: null},
  {Title: 'C', ReadUnderstood: false},
  {Title: 'E', ReadUnderstood: null},
  {Title: 'B', ReadUnderstood: true},
  {Title: 'D', ReadUnderstood: true},
  {Title: 'F', ReadUnderstood: null},
];


items.sort((a, b) => {
  let aReadUnderstood = a.ReadUnderstood !== false;
  let bReadUnderstood = b.ReadUnderstood !== false;
  if(aReadUnderstood !== bReadUnderstood)
    return aReadUnderstood - bReadUnderstood;
  else
    return a.Title.localeCompare(b.Title); 
});

console.log(items);

With a library like lodash you can get this a bit shorter and terser:

let items = [
  {Title: 'A', ReadUnderstood: null},
  {Title: 'C', ReadUnderstood: false},
  {Title: 'E', ReadUnderstood: null},
  {Title: 'B', ReadUnderstood: true},
  {Title: 'D', ReadUnderstood: true},
  {Title: 'F', ReadUnderstood: null},
];

items = _.sortBy(items, e => [e.ReadUnderstood !== false, e.Title]);
console.log(items);
<script src="https://unpkg.com/[email protected]/lodash.min.js"></script>

Comments

0

try

items.sort((a,b) => a.ReadUnderstood===false ? 
  -1 : (b.ReadUnderstood===false ? 1 : a.Title.localeCompare(b.Title)));

items = [
    {Title: 'A', ReadUnderstood: null},
    {Title: 'C', ReadUnderstood: false},
    {Title: 'E', ReadUnderstood: null},
    {Title: 'B', ReadUnderstood: true},
    {Title: 'D', ReadUnderstood: true},
    {Title: 'F', ReadUnderstood: null},
]

items.sort((a,b) => a.ReadUnderstood===false ? 
      -1 : (b.ReadUnderstood===false ? 1 : a.Title.localeCompare(b.Title)));
      
console.log(items);

3 Comments

You need to compare the ReadUnderstood between the two objects otherwise the sort won't work correctly. When the C item is the first item in the array, your sort would sort it somewhere in the middle instead of at the top
Here's an example where it would fail: es6console.com/jze1oz4o
Almost, you also need to handle the case where both are false, then they need to be sorted alphabetically. e.g. C should come before G in this example: es6console.com/jze23c0m
0

Here is another option. First partitioning the array based on item.ReadUnderstood === false. Then sorting both arrays and concatenating them back together.

Array.prototype.partition = function (callback, thisArg) {
  if (thisArg !== undefined) callback = callback.bind(thisArg);
  
  const partitions = { true: [], false: [] };
  this.forEach((item, ...args) => partitions[!!callback(item, ...args)].push(item));
  return [partitions[true], partitions[false]];
};

const items = [
  {Title: 'A', ReadUnderstood: null},
  {Title: 'C', ReadUnderstood: false},
  {Title: 'E', ReadUnderstood: null},
  {Title: 'B', ReadUnderstood: true},
  {Title: 'D', ReadUnderstood: true},
  {Title: 'F', ReadUnderstood: null},
];

const [ruFalse, ruOther] = items.partition(item => item.ReadUnderstood === false);
const byTitle = ({Title: t1}, {Title: t2}) => t1.localeCompare(t2);
console.log(ruFalse.sort(byTitle).concat(ruOther.sort(byTitle)));

If you don't care about ruFalse being sorted you can leave out .sort(byTitle) for that list.

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.