0

I have an array that contains negative numbers, empty string '', Zero 0 and positive numbers. I want to sort the array so that empty string '' always shows at the end.

[-1, 2, '', -2, 0, 4, '', 1,5,0, -5, 7, 11, '',3]

and expected output

[-5, -2, -1, 0, 0, 1, 2, 3, 4, 5, 7, 11, '', '', '']
StaticName
  • 239
  • 1
  • 2
  • 10
  • 2
    What did you do to achieve this expected result? Where did you get stuck? Can you share your existing/problematic code, and a description of the problems? Are all numbers in the Array as numbers (`[0, 1, 2]`), or will there be numeric strings (`[0, "1", 2]`), what should happen in those cases? – David Thomas Apr 04 '23 at 14:59
  • @DavidThomas applied at least 10 examples from stackoverflow but all of them mixup 0 and `''` together. If you read carefully, you will see that it consists of numbers and empty string – StaticName Apr 04 '23 at 15:01
  • 1
    are you sure you searched well? https://stackoverflow.com/questions/33016087/js-sort-empty-to-end – Chris G Apr 04 '23 at 15:02
  • @StaticName, yes. I saw the empty strings. What I didn't see were any numeric strings - as shown in my question - so I asked what should happen with those. – David Thomas Apr 04 '23 at 15:05
  • Create an intermediate integer array by looping on your array, counting empty strings (without adding them to the intermediate array) and adding the integers resulting from string conversion. Then sort the integer array before adding the count of empty strings. – Graffito Apr 04 '23 at 21:52

4 Answers4

2

You can prioritize the comparison with the empty string in the sorting condition.

let arr = [-1, 2, '', -2, 0, 4, '', 1,5,0, -5, 7, 11, '',3];
arr.sort((a, b) => (a === '') - (b === '') || a - b);
console.log(arr);
Unmitigated
  • 76,500
  • 11
  • 62
  • 80
1

you can write a custom sort function so that empty strings are moved to the back of the array
[...arr] if you don't want to mutate your original array since sort mutates original array.

const arr = [-1, 2, '', -2, 0, 4, '', 1,5,0, -5, 7, 11, '',3];

const sorted = [...arr].sort((a, b) => {
  if (a === '') return 1;   //a after b
  if (b === '') return -1;  //b after a
  return a - b;             //number sorting
});

//or shorten using ternary operator
//const sorted = [...arr].sort((a, b) => a === '' ? 1 : b === '' ? -1 : a - b);

console.log(sorted);
cmgchess
  • 7,996
  • 37
  • 44
  • 62
0

If b is empty, place it in front; but if a is empty, place it at the end.

Where both a and b are non-empty, perform a standard a - b return.

const arr = [-1, 2, '', -2, 0, 4, '', 1, 5, 0, -5, 7, 11, '', 3];

arr.sort((a, b) => {
  if (b === '') return -1;
  if (a === '') return 1;
  return a - b;
});

console.log(JSON.stringify(arr));

If you want to always keep the empty values at the end, you need to track sorting direction:

const
  DIRECTION_ASC = 1,
  DIRECTION_DESC = -1,
  customSort = (direction = DIRECTION_ASC) => {
    const dir = direction === DIRECTION_DESC ? -1 : 1;
    return (a, b) => {
      if (b === '') return -1;
      if (a === '') return 1;
      return (a - b) * dir;
    };
  };

const
  custSortAsc = customSort(),
  custSortDesc = customSort(DIRECTION_DESC),
  sortTextJsonArr = (textarea, sorter) => {
    textarea.value = JSON.stringify(
        JSON.parse(textarea.value.replace(/'/g, '"'))
          .sort(sorter))
      .replace(/"/g, "'")
      .replace(/,/g, ', ');
  },
  sortAsc = (selector) =>
    sortTextJsonArr(document.querySelector(selector), custSortAsc),
  sortDesc = (selector) =>
    sortTextJsonArr(document.querySelector(selector), custSortDesc);
body, textarea { text-align: center; }
<textarea id="data" rows="2" cols="60">
[-1, 2, '', -2, 0, 4, '', 1, 5, 0, -5, 7, 11, '', 3]
</textarea>
<br />
<button onclick="sortAsc('#data')">Sort Ascending</button>
<button onclick="sortDesc('#data')">Sort Descending</button>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
0

I would personally define some general purpose sort helpers that are useful in all sorts of sort() scenarios.

Let's first look at the answer without knowing what the helpers do:

import { fallThrough, by, asc, desc, front, back } from './lib/sort-helpers';

const items = [-1, 2, '', -2, 0, 4, '', 1,5,0, -5, 7, 11, '',3];
items.sort(fallThrough(
  back(item => typeof item === "string"),
  asc,
));
console.log(items);

function solution({ fallThrough, by, asc, desc, front, back, itself }) {
  const items = [-1, 2, '', -2, 0, 4, '', 1,5,0, -5, 7, 11, '',3];
  items.sort(fallThrough(
    back(item => typeof item === "string"),
    asc,
  ));
  console.log(items);
}

const SortHelpers = (function () {
  const fallThrough = (...comparators) => (a, b) => (
    comparators.reduce((diff, comparator) => diff || comparator(a, b), 0)
  );
  const by = (fn, comparator = asc) => (a, b) => comparator(fn(a), fn(b));
  const asc = (a, b) => -(a < b) || +(a > b);
  const desc = (a, b) => asc(b, a);
  const bool = (fn) => (item) => !!fn(item);
  const front = (fn) => by(bool(fn), desc);
  const back = (fn) => by(bool(fn), asc);
  
  return { fallThrough, by, asc, desc, front, back };
})();

solution(SortHelpers);

As you can see this solution looks relatively simple. Note that the second sorting criteria asc will apply to both numbers and strings. So if the strings are non-empty, they will also be sorted in ascending order.

Helpers

Now, let's explain the general purpose helpers (see collapsed snippet).

I'll use the term comparator quite a bit. A comparator is a function that accepts 2 arguments, then returns a number to tell sort() how the 2 arguments should be sorted.

A negative value indicates that the first argument should be placed before the second. A positive value indicates that the first argument should be placed after the second argument. Zero indicates that both arguments can be considered equal.

A basic example of an comparator can be seen in the explanation for asc.

fallThrough

fallThrough allows us to pass multiple comparator functions. If the first comparator says that the values are equal, then the next comparator is invoked. If the second comparator also says the values are equal, we'll move on the the third.

This goes on until one of the comparators says which value comes first. If there are no more comparators available, the values are considered equal.

Here is an example without the use of the other helpers:

const people = [
  { firstName: "John", lastName: "Doe" },
  { firstName: "Jane", lastName: "Doe" },
  { firstName: "Dave", lastName: "Dex" },
];

people.sort(fallThrough(
  (personA, personB) => personA.lastName.localeCompare(personA.lastName),
  (personA, personB) => personB.firstName.localeCompare(personB.firstName),
));

The above will first try to sort based on last name. If those are equal we sort by first name.

by

by is a simple helper that accepts a mapping function and a comparator. It applies the mapping function to both comparator arguments. The purpose is mainly to reduce code repetition.

When looking in the example above we can see that we access personA.lastName and personB.lastName, which are the same mappings for both compared items. By using by we can simplify this example:

const localeCompare = (a, b) => a.localeCompare(b);

people.sort(fallThrough(
  by(person => person.LastName, localeCompare),
  by(person => person.firstName, localeCompare),
));

Note that you don't have to use fallThrough. If have just a single sorting criteria then the following will suffice:

people.sort(by(person => person.LastName, localeCompare));

asc & desc

asc and desc are comparators based on < (less than) and > (greater then).

The definition of asc is:

const asc = (a, b) => -(a < b) || +(a > b);

Which might be hard to read if you're still new to JavaScript. It could also be written as:

function asc(a, b) {
  if (a < b) return -1; // a before b
  if (a > b) return  1; // a after b
  return 0; // equal
}

The more cryptic version uses -(a < b), which evaluates to -(true), which in turn evaluates to -1. Since -1 is a truthy value || short circuits and -1 is returned. If this first expression is not true, then it evaluates to -(false), which will produce -0. Since -0 is a falsy value we'll move on to the second operand of ||. Here we'll do the same if a > b is true then +(true) will result in 1 for the return value. If a > b is false (aka the values are equal) then +(false) will return 0.

Note that when comparing strings with < and > they are compared based on code point value of the characters.

desc is the same as asc, but produces the reverse order by swapping the comparator arguments.

front & back

front and back are there to simplify boolean logic in sorting.

by(item => typeof item === "string", asc)

Is kind of hard to read. Are the strings placed in the front or in the back? Let's think for a second. typeof item === "string" produces a true or false value. true will coerce into 1, false into 0. We are sorting in ascending order, so we are sorting from small to high. 1 is greater than 0, meaning that strings are placed in the back.

You don't want to do this kind of thinking when reading code. Therefore I've provided the front and back helpers. These expect you to pass a test and make the above a lot more readable.

back(item => typeof item === "string")

In this version you can instantly see that strings are placed at the back of the array.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52