2

I have an array of objects that I need sorted. If the object has class "search-filter-type", it needs to be sorted by specific order (see: car_types_order). For all other objects in the array, it should be sorted alphabetically by label. What I have below works, I'm wondering if there should be two separate sort functions or if there is a cleaner way of achieving this? Thanks!

Code:

    var car_types_order = ['Small Cars', 'Medium Cars', 'Large Cars', 'SUVs & Crossovers', 'Vans', 'Luxury',
        'Convertibles', 'Sports', 'Wagons', 'Hybrids', 'Electric', 'Pickup Trucks', 'Off-Road Vehicles', 'RVs',
        'Commercial', 'Specialty'];

    filters.sort(function(a, b){
        if (a.class == "search-filter-type" || b.class == "search-filter-type")
            return car_types_order.indexOf(a.label) - car_types_order.indexOf(b.label);
        else
        {
            if (a.label < b.label)
                return -1;
            if (a.label > b.label)
                return 1;
        }
        return 0;
    });

Example Objects:

{title: "Rental Company", class: "search-filter-company", label: "Hertz", id: "filter-companies-HZ", value: "HZ"}
{title: "Rental Company", class: "search-filter-company", label: "Silvercar", id: "filter-companies-SC", value: "SC"}
{title: "Rental Company", class: "search-filter-company", label: "NextCar", id: "filter-companies-NC", value: "NC"}
{title: "Rental Company", class: "search-filter-company", label: "National", id: "filter-companies-NA", value: "NA"}
{title: "Rental Company", class: "search-filter-company", label: "Payless", id: "filter-companies-ZA", value: "ZA"}
{title: "Rental Company", class: "search-filter-company", label: "Sixt", id: "filter-companies-SX", value: "SX"}
{title: "Rental Company", class: "search-filter-company", label: "Thrifty", id: "filter-companies-ZT", value: "ZT"}
{title: "Car Type", class: "search-filter-type", label: "Commercial", id: "filter-types-commercial", value: "SKAR"}
{title: "Rental Company", class: "search-filter-company", label: "Routes", id: "filter-companies-RO", value: "RO"}
{title: "Car Type", class: "search-filter-type", label: "Electric", id: "filter-types-electric", value: "ICAE"}
{title: "Car Type", class: "search-filter-type", label: "Medium Cars", id: "filter-types-medium", value: "FCAR"}
{title: "Car Type", class: "search-filter-type", label: "Small Cars", id: "filter-types-small", value: "CCAR"}
Michael
  • 403
  • 1
  • 9
  • 28
  • 2
    Since you are doing a custom sort, IMO, I think what you have is fine. You could break down both ways of sorting into functions and then it would be re-usable, which may be a way to improve on your design. But the sorting itself, needs to still implement the logic you have. – Ryan Wilson May 01 '18 at 18:00
  • do you want alphabetically ordered at bottom or at top? – Nina Scholz May 01 '18 at 18:02
  • It doesn't necessarily need to be ordered by `class`. The labels themselves need to be ordered though (for search-filter-type) it should follow car_types_order. Whereas for search-filter-company it should be alphabetical. – Michael May 01 '18 at 18:04
  • Don't look for "clever" hacks to shorten code, especially when they involve unintuitive operations requiring unusual, implicit type coercions. Shorter does not mean clearer. You code should describe clearly what you're intending. –  May 01 '18 at 19:48

2 Answers2

1

You could first check if 'search-filter-type' is given and sort this items to botton, if both given, the sort by index and if same, then sort by alphabet.

For sorting 'search-filter-type' to top, you could swap a and b in this line:

(a.class === "search-filter-type") - (b.class === "search-filter-type")

var data = [{ title: "Rental Company", class: "search-filter-company", label: "Hertz", id: "filter-companies-HZ", value: "HZ" }, { title: "Rental Company", class: "search-filter-company", label: "National", id: "filter-companies-NA", value: "NA" }, { title: "Rental Company", class: "search-filter-company", label: "NextCar", id: "filter-companies-NC", value: "NC" }, { title: "Rental Company", class: "search-filter-company", label: "Payless", id: "filter-companies-ZA", value: "ZA" }, { title: "Rental Company", class: "search-filter-company", label: "Silvercar", id: "filter-companies-SC", value: "SC" }, { title: "Rental Company", class: "search-filter-company", label: "Sixt", id: "filter-companies-SX", value: "SX" }, { title: "Rental Company", class: "search-filter-company", label: "Thrifty", id: "filter-companies-ZT", value: "ZT" }, { title: "Car Type", class: "search-filter-type", label: "Commercial", id: "filter-types-commercial", value: "SKAR" }, { title: "Rental Company", class: "search-filter-company", label: "Routes", id: "filter-companies-RO", value: "RO" }, { title: "Car Type", class: "search-filter-type", label: "Electric", id: "filter-types-electric", value: "ICAE" }, { title: "Car Type", class: "search-filter-type", label: "Medium Cars", id: "filter-types-medium", value: "FCAR" }, { title: "Car Type", class: "search-filter-type", label: "Small Cars", id: "filter-types-small", value: "CCAR" }],
    car_types_order = ['Small Cars', 'Medium Cars', 'Large Cars', 'SUVs & Crossovers', 'Vans', 'Luxury', 'Convertibles', 'Sports', 'Wagons', 'Hybrids', 'Electric', 'Pickup Trucks', 'Off-Road Vehicles', 'RVs', 'Commercial', 'Specialty'];

data.sort((a, b) =>
    (a.class === "search-filter-type") - (b.class === "search-filter-type")
        || car_types_order.indexOf(a.label) - car_types_order.indexOf(b.label)
        || a.label.localeCompare(b.label)
);

console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Some more advanced techniques:

  • Use an object for accessing the order of car types.

  • Take a default value for not found types. This requires not to use a falsy value, like zero, for known types.

var data = [{ title: "Rental Company", class: "search-filter-company", label: "Hertz", id: "filter-companies-HZ", value: "HZ" }, { title: "Rental Company", class: "search-filter-company", label: "National", id: "filter-companies-NA", value: "NA" }, { title: "Rental Company", class: "search-filter-company", label: "NextCar", id: "filter-companies-NC", value: "NC" }, { title: "Rental Company", class: "search-filter-company", label: "Payless", id: "filter-companies-ZA", value: "ZA" }, { title: "Rental Company", class: "search-filter-company", label: "Silvercar", id: "filter-companies-SC", value: "SC" }, { title: "Rental Company", class: "search-filter-company", label: "Sixt", id: "filter-companies-SX", value: "SX" }, { title: "Rental Company", class: "search-filter-company", label: "Thrifty", id: "filter-companies-ZT", value: "ZT" }, { title: "Car Type", class: "search-filter-type", label: "Commercial", id: "filter-types-commercial", value: "SKAR" }, { title: "Rental Company", class: "search-filter-company", label: "Routes", id: "filter-companies-RO", value: "RO" }, { title: "Car Type", class: "search-filter-type", label: "Electric", id: "filter-types-electric", value: "ICAE" }, { title: "Car Type", class: "search-filter-type", label: "Medium Cars", id: "filter-types-medium", value: "FCAR" }, { title: "Car Type", class: "search-filter-type", label: "Small Cars", id: "filter-types-small", value: "CCAR" }],
    car_types_order = ['Small Cars', 'Medium Cars', 'Large Cars', 'SUVs & Crossovers', 'Vans', 'Luxury', 'Convertibles', 'Sports', 'Wagons', 'Hybrids', 'Electric', 'Pickup Trucks', 'Off-Road Vehicles', 'RVs', 'Commercial', 'Specialty'],
    order = Object.assign({ default: 0 }, ...car_types_order.map((k, v) => ({ [k]: v + 1 })));

data.sort((a, b) =>
    (a.class === "search-filter-type") - (b.class === "search-filter-type")
        || (order[a.label] || order.default) - (order[b.label] || order.default)
        || a.label.localeCompare(b.label)
);

console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • There's a potential flaw in the logic. It will work reliably only as long as objects that do *not* have the `"search-filter-type"` class are also *guaranteed* to *not* have non-matching `.label`s in that array. If they do, then they'll be sorted by that array order instead of lexically. –  May 01 '18 at 20:10
  • ...There's also a negative performance implication to performing all those unnecessary `.indexOf()` operations. –  May 01 '18 at 20:12
  • @CrazyTrain, the data structure is given as is. the `indexOf` operation could be replaced by an object with order types as keys and indices as values, as [here](https://stackoverflow.com/questions/50028512/sort-an-object-array-by-custom-order/50028663#50028663) mentioned, or [here](https://stackoverflow.com/questions/47158756/sort-an-array-of-object-by-a-property-with-custom-order-not-alphabetically/47158807#47158807). – Nina Scholz May 01 '18 at 20:16
  • *"the data structure is given as is"* How do you know? Did you post this question? Is that a strawman account? –  May 01 '18 at 20:26
  • @CrazyTrain, every object contains the same properties in the question. i do not assume that there are some properties in some objects missing. this is not part of the question and should be mentioned, if so. – Nina Scholz May 01 '18 at 20:29
  • This isn't about the objects having extra properties. The problem comes from those objects that should not be filtered by the array, but will be if their `.label` property have a value that also happen to match items in that array. –  May 01 '18 at 20:32
  • i do not understand the filtering part. – Nina Scholz May 01 '18 at 20:34
  • Sorry, I meant to write "sorted", not "filtered". They're using the word `filter` in the class. –  May 01 '18 at 20:35
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/170179/discussion-between-nina-scholz-and-crazy-train). – Nina Scholz May 01 '18 at 20:36
1

Don't be allured by "clever" hacks that provide short code that becomes very difficult to interpret, and therefore likely to obscure nasty bugs.

Write clear, descriptive code so that others (and even yourself at a later date) can quickly comprehend its intent. Here's an example:

var data = [{ title: "Rental Company", class: "search-filter-company", label: "Hertz", id: "filter-companies-HZ", value: "HZ" }, { title: "Rental Company", class: "search-filter-company", label: "National", id: "filter-companies-NA", value: "NA" }, { title: "Rental Company", class: "search-filter-company", label: "NextCar", id: "filter-companies-NC", value: "NC" }, { title: "Rental Company", class: "search-filter-company", label: "Payless", id: "filter-companies-ZA", value: "ZA" }, { title: "Rental Company", class: "search-filter-company", label: "Silvercar", id: "filter-companies-SC", value: "SC" }, { title: "Rental Company", class: "search-filter-company", label: "Sixt", id: "filter-companies-SX", value: "SX" }, { title: "Rental Company", class: "search-filter-company", label: "Thrifty", id: "filter-companies-ZT", value: "ZT" }, { title: "Car Type", class: "search-filter-type", label: "Commercial", id: "filter-types-commercial", value: "SKAR" }, { title: "Rental Company", class: "search-filter-company", label: "Routes", id: "filter-companies-RO", value: "RO" }, { title: "Car Type", class: "search-filter-type", label: "Electric", id: "filter-types-electric", value: "ICAE" }, { title: "Car Type", class: "search-filter-type", label: "Medium Cars", id: "filter-types-medium", value: "FCAR" }, { title: "Car Type", class: "search-filter-type", label: "Small Cars", id: "filter-types-small", value: "CCAR" }],
    car_types_order = ['Small Cars', 'Medium Cars', 'Large Cars', 'SUVs & Crossovers', 'Vans', 'Luxury', 'Convertibles', 'Sports', 'Wagons', 'Hybrids', 'Electric', 'Pickup Trucks', 'Off-Road Vehicles', 'RVs', 'Commercial', 'Specialty'];

data.sort((a, b) => {
  const aSearchArray = a.class === "search-filter-type";
  const bSearchArray = b.class === "search-filter-type";

  if (aSearchArray !== bSearchArray) { // Group by ordering method
     return aSearchArray ? 1 : -1;
  }

  if (aSearchArray) { // They both need to search the array
     return car_types_order.indexOf(a.label) - car_types_order.indexOf(b.label);
  }

  return a.label.localeCompare(b.label); // Order them lexically
});

console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }

This is guaranteed to never search the array unless both objects have the "search-filter-type" class. Nor will it perform a lexical comparison of .label if neither have that class.


As a potential performance enhancement, use a Map of strings to order number instead of an Array for the car_types_order.

This demo starts with an Array but uses it only to build the Map.

var data = [{ title: "Rental Company", class: "search-filter-company", label: "Hertz", id: "filter-companies-HZ", value: "HZ" }, { title: "Rental Company", class: "search-filter-company", label: "National", id: "filter-companies-NA", value: "NA" }, { title: "Rental Company", class: "search-filter-company", label: "NextCar", id: "filter-companies-NC", value: "NC" }, { title: "Rental Company", class: "search-filter-company", label: "Payless", id: "filter-companies-ZA", value: "ZA" }, { title: "Rental Company", class: "search-filter-company", label: "Silvercar", id: "filter-companies-SC", value: "SC" }, { title: "Rental Company", class: "search-filter-company", label: "Sixt", id: "filter-companies-SX", value: "SX" }, { title: "Rental Company", class: "search-filter-company", label: "Thrifty", id: "filter-companies-ZT", value: "ZT" }, { title: "Car Type", class: "search-filter-type", label: "Commercial", id: "filter-types-commercial", value: "SKAR" }, { title: "Rental Company", class: "search-filter-company", label: "Routes", id: "filter-companies-RO", value: "RO" }, { title: "Car Type", class: "search-filter-type", label: "Electric", id: "filter-types-electric", value: "ICAE" }, { title: "Car Type", class: "search-filter-type", label: "Medium Cars", id: "filter-types-medium", value: "FCAR" }, { title: "Car Type", class: "search-filter-type", label: "Small Cars", id: "filter-types-small", value: "CCAR" }],
    car_types_order = new Map(['Small Cars', 'Medium Cars', 'Large Cars', 'SUVs & Crossovers', 'Vans', 'Luxury', 'Convertibles', 'Sports', 'Wagons', 'Hybrids', 'Electric', 'Pickup Trucks', 'Off-Road Vehicles', 'RVs', 'Commercial', 'Specialty'].map((s, i) => [s, i+1]));

data.sort((a, b) => {
  const aSearchMap = a.class === "search-filter-type";
  const bSearchMap = b.class === "search-filter-type";

  if (aSearchMap !== bSearchMap) { // Group by ordering method
     return aSearchMap ? 1 : -1;
  }

  if (aSearchMap) { // They both need to search the map
     return (car_types_order.get(a.label) || 0) - (car_types_order.get(b.label) || 0);
  }

  return a.label.localeCompare(b.label); // Order them lexically
});

console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
  • you might review this part: `car_types_order.get(a.label) || 0 - car_types_order.get(b.label) || 0`, which is basically `car_types_order.get(a.label) || (0 - car_types_order.get(b.label)) || 0`. anyway, be nice. – Nina Scholz May 03 '18 at 09:33
  • @NinaScholz: Yep, you're right. I didn't consider precedence properly. Thanks for pointing out the error in my code. I updated the answer to fix the problem. –  May 03 '18 at 12:36