'lodash orderby with null and real values not ordering correctly

I have an Angular 2 typescript application that is using lodash for various things.

I have an array of objects that I am ordering using a property in the object...

_.orderBy(this.myArray, ['propertyName'], ['desc']);

This works well however my problem is that sometimes 'propertyName' can have a null value. These are ordered as the first item in a descending list, the highest real values then follow.

I want to make these null values appear last in the descending ordering.

I understand why the nulls come first.

Does anyone know how to approach this?



Solution 1:[1]

The _.orderBy() function's iteratees can use a method instead of a string. Check the value, and if it's null return an empty string.

const myArray = [{ propertyName: 'cats' }, { propertyName: null }, { propertyName: 'dogs' }, { propertyName: 'rats' }, { propertyName: null }];

const result = _.orderBy(myArray, ({ propertyName }) => propertyName || '', ['desc']);

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.min.js"></script>

The check can be simple (like the one I've used), which converts all falsy values to an empty string:

propertyName || ''

If you need a stricter check, you can use the ternary operator, and handle just null values:

propertyName === null ? '' : propertyName

Edit: Example with multiple ordering:

const result = _.orderBy(myArray, (item) => [get(item, 'propertyName', 0), get(item, 'propertyName2')], ['desc', 'asc']);

This will order by propertyName then propertyName2.

If propertyName is undefined/null then its default order will be set to 0. (and therefore will be displayed at last because of desc ordering on the propertyName field). In such case, propertyName2 will therefore determine the ordering.

Solution 2:[2]

Just for future reference to others you can do this to sort ascending with falsey values at the end.

items =>
  orderBy(
    items,
    [
      i => !!i.attributeToCheck,
      i => {
        return i.attributeToCheck ? i.attributeToCheck.toLowerCase() : ''
      }
    ],
    ['desc', 'asc']
  )

Solution 3:[3]

This will put bad values at the bottom, and it differentiates between numbers and strings.

const items = [] // some list

const goodValues = isAscending => ({ value }) => {
    if (typeof value !== 'string' && isNaN(value)) {
        return isAscending ? Infinity : -Infinity
    }

    return value || ''
}

const sortedItems = orderBy(
    items,
    [goodValues(isAscending), 'value'],
    [isAscending ? 'asc' : 'desc']
)

Solution 4:[4]

This worked for me

orders = [{id : "1", name : "test"}, {id : "1"}];
sortBy = ["id", "name"];
orderby(
            orders,
            sortBy.map(s => {
                return (r: any) => {
                    return r[s] ? r[s] : "";
                };
            })),
        );

Solution 5:[5]

mine looks like this. PropName and sort are both variables in my solution

return _.orderBy( myarray, [
  ( data ) => {
    if ( data[propName] === null ) {
        data[propName] = "";
    }
    return data[propName].toLowerCase();
    }
 ], [sort] );

I wanted tolowercase because otherwise the sorting is not correct if different casings

Solution 6:[6]

I created a function for this (ts code):

const orderByFix = (array: any[], orderKeys: string[], orderDirs: ('asc' | 'desc')[]) => {
  const ordered = orderBy(array, orderKeys, orderDirs);
  const withProp = ordered.filter((o) => orderKeys.every(k => o[k]));
  const withoutProp = ordered.filter((o) => !orderKeys.every(k => o[k]));
  return [...withProp, ...withoutProp];
};

Solution 7:[7]

I've extended gwendall's answer to also handle case when "order keys" are functions (_.orderBy allows that)

const orderByFix = (
  array: any[],
  orderKeys: (string | ((o: any) => any))[],
  orderDirs: ('asc' | 'desc')[]
) => {
  const ordered = orderBy(array, orderKeys, orderDirs)
  const withProp = ordered.filter((o) =>
    orderKeys.every((k) => {
      if (typeof k === 'string') {
        return o[k]
      } else if (typeof k === 'function') {
        return k(o)
      } else {
        throw Error(`Order key must be string or function not ${typeof k}`)
      }
    })
  )
  const withoutProp = ordered.filter(
    (o) =>
      !orderKeys.every((k) => {
        if (typeof k === 'string') {
          return o[k]
        } else if (typeof k === 'function') {
          return k(o)
        } else {
          throw Error(`Order key must be string or function not ${typeof k}`)
        }
      })
  )
  return [...withProp, ...withoutProp]
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 infinity
Solution 3 Stephen
Solution 4 lars1595
Solution 5 Johansrk
Solution 6 gwendall
Solution 7 lukaszb