38

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?

Ben Cameron
  • 4,335
  • 6
  • 51
  • 77

8 Answers8

45

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.

Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • Very close, thank you. I've posted the full solution below. – Ben Cameron Dec 14 '16 at 10:10
  • 3
    Why do you need all the extra stuff `[( o ) => { return o.myProperty || ''}]`? My solution works out of the box. See the snippet. Just replace `propertyName` with the name of the property. – Ori Drori Dec 14 '16 at 10:29
  • It didn't seem to work for me the way you had it but it did the way I had it. Maybe its a lodash thing or something. In any case the problem is solved now. Thanks for your help. – Ben Cameron Dec 14 '16 at 10:52
  • Weird. Maybe typescript doesn't like it. – Ori Drori Dec 14 '16 at 11:11
  • 2
    this still sorts nulls first in 'asc' order, is there a ways have null always at end ? – stallingOne Jul 23 '18 at 09:29
  • @stallingOne - it's not an elegant solution, but you can replace it with some later characters, such as a sequence of tildes - ~~~~~~~~~~~~. A case like this is better handled with the JS sort. – Ori Drori Jul 23 '18 at 10:38
  • Should be the accepted answer. @BenCameron would you mind accept it? – Vadorequest Dec 04 '19 at 17:49
24

The code I needed looks like this...

_.orderBy(this.myArray, [( o ) => { return o.myProperty || ''}], ['desc']); 
Ben Cameron
  • 4,335
  • 6
  • 51
  • 77
7

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']
  )
infinity
  • 530
  • 5
  • 6
1

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

Johansrk
  • 5,160
  • 3
  • 38
  • 37
1

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']
)
Stephen
  • 7,994
  • 9
  • 44
  • 73
1

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] : "";
                };
            })),
        );
lars1595
  • 886
  • 3
  • 12
  • 27
1

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];
};
gwendall
  • 920
  • 15
  • 22
0

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]
}
lukaszb
  • 694
  • 1
  • 6
  • 15