0

I want to sort an array of object (of the same interface) firstly sorting by one attribute of this interface, if these attributes are equals then sorting by secondly attribute, then sorting by a third attribute and so on. And all in typescript mode.

My attempt solutions:

  1. Define the sorter:

    type Sorter = (a: T, b: T) => number;

Example:

const stateSorter = (aState: number, bState: number) => (bState < aState? -1 : 1);
const dateSorter = (aDate: Date, bDate: Date) => (aDate >= bDate ? -1 : 1);
  1. Define a sorter around attribute of a interface T

    interface SortSupplier<T, R> { supplier: (container: T) => R; sorter: Sorter< R >; }

Example:

interface StateAndDate {
  stateId: ClientStateIdEnum;
  registrationDate: Date;
}
const sorterBySupplier: SortSupplier<StateAndDate, Date> = {supplier: container => container.registrationDate, sorter: dateSorter}

First incorrect attempt:

Do by array of priority sort of SortSupplier:

const sortBySorters1 = <T>(a: T, b: T, orderedSorters: SortSupplier<T, any>[]): number => {
  let sortResult = 0;
  orderedSorters.some(supplierSorter => {
    sortResult = supplierSorter.sorter(supplierSorter.supplier(a), supplierSorter.supplier(b));
    return sortResult !== 0;
  });
  return sortResult;
}

and one wrong example is:

(a: StateAndDate, b: StateAndDate) => {
  return sortBySorters1(a, b, [
    {supplier: container => container.stateId, sorter: stateSorter},
    {supplier: container => container.registrationDate, sorter: stateSorter}
  ]);
}

It is wrong because the second sorter must sort by date but it accepts other different sorter, by number (state), due to the use of any as the second generic type of the SortSupplier

Second incorrect attempt:

Create a type of all possible attributes types of SortSupplier, for example, numbers and dates:

type SortSupplierType<T> = SortSupplier<T, Date>|SortSupplier<T, number>;

Then the previous incorrect sorter configuration:

{supplier: container => container.registrationDate, sorter: stateSorter} 

gives compilation error and the only compliance solution is the rigth one:

(a: StateAndDate, b: StateAndDate) => {
  return sortBySorters1(a, b, [
    {supplier: container => container.stateId, sorter: stateSorter},
    {supplier: container => container.registrationDate, sorter: dateSorter}
  ]);
}

But:

const sortBySorters2 = <T>(a: T, b: T, orderedSorters: SortSupplierType<T>[]): number => {
  let sortResult = 0;
  orderedSorters.some(supplierSorter => {
    sortResult = supplierSorter.sorter(supplierSorter.supplier(a), supplierSorter.supplier(b));
    return sortResult !== 0;
  });
  return sortResult;
}

But it gives the next compilation error:

Argument of type 'number | Date' is not assignable to parameter of type 'Date & number'.

Type 'number' is not assignable to type 'Date & number'.

Type 'number' is not assignable to type 'Date'.

sortResult = supplierSorter.sorter(supplierSorter.supplier(a), supplierSorter.supplier(b));

¿Someone has a generalized solution to this sort problem around cascading priorioty sort of attributes interface?


(Sorry for my English)

PacoBlanco
  • 25
  • 5
  • I'm wondering how your question is different from [How to sort an array of objects by multiple fields?](https://stackoverflow.com/q/6913512/1563833) – Wyck Jun 10 '23 at 03:39

2 Answers2

0

You can accept the items and keys as the arguments to the function and then just iterate over the keys and order each item based on its value. If the values are same for a key, move to next key.

interface Item {
  a: string;
  b: number;
  c: Date;
}

function sortByKeys<T>(items: T[], keys: (keyof T)[]): void {
  items.sort((item1, item2) => {
    for (const key of keys) {
      const v1 = item1[key];
      const v2 = item2[key];
      if (v1 < v2) return -1;
      if (v2 < v1) return 1;
    }
    return 0;
  });
}

const items: Item[] = [
  { a: 'aa', b: 1, c: new Date() },
  { a: 'a', b: 2, c: new Date() },
  { a: 'a', b: 1, c: new Date() },
];

sortByKeys(items, ['b', 'a']);

console.log(items);

If you need custom sorters, you could do the following:

interface Item {
  a: string;
  b: number;
  c: Date;
}

type CustomSorter<T> = (value1: T, value2: T) => -1 | 0 | 1;

function sortByKeys<T>(
  items: T[], 
  keys: (keyof T)[], 
  customSorters: Partial<{ [key in (keyof T)]: CustomSorter<T[key]>}> = {}
): void {
  items.sort((item1, item2) => {
    for (const key of keys) {
      const v1 = item1[key];
      const v2 = item2[key];
      const customSorter = customSorters[key];
      if (customSorter) {
        const result = customSorter(v1, v2);
        if (result != 0) return result;
      } else {
        if (v1 < v2) return -1;
        if (v2 < v1) return 1;
      }
    }
    return 0;
  });
}

const randomDate = () => new Date(Date.now() + Math.floor(Math.random() * 1000));

const items: Item[] = [
  { a: 'aa', b: 1, c: randomDate() },
  { a: 'a', b: 2, c: randomDate() },
  { a: 'a', b: 1, c: randomDate() },
];

sortByKeys(items, ['c'], {
  'c': (date1, date2) => {
    if (date1 < date2) return -1;
    if (date2 < date1) return 1;
    return 0;
  },
});

console.log(items);
vighnesh153
  • 4,354
  • 2
  • 13
  • 27
  • Very great solution! Only I do a simple modification to force that 'customSorters' attribute must have only and all keys defined in 'keys' attribute. – PacoBlanco Jun 10 '23 at 13:25
  • If you enforce that, you will have to mandatorily provide `customSorter` for keys that have primitive values like strings or numbers. Sorting of strings and numbers can easily be handled by a generic logic as shown above. Also, if you have multiple keys with values of type string or number, you will have to duplicate the customSorter for each key. – vighnesh153 Jun 10 '23 at 13:42
0
// maps tuple to a new tuple
type Mapping<T, A extends any[]> = [...{ [K in keyof A]: {
    get: (e: T) => A[K],
    cmp: (a: A[K], b: A[K]) => number
} }]


function sortByMany<T, const A extends any[]>(list: T[], sorters: Mapping<T, A>): T[] {
    return list.sort((a, b) => {
        for (let { get, cmp } of sorters) {
            let v = cmp(get(a), get(b))
            if (v) return v;
        }
        return 0;
    })
}

let a = sortByMany<number, [number, string]>([16, 22, 34, 73], [
    { get: e => e % 10, cmp: (a, b) => a - b },
    { get: e => e % 10 + '', cmp: (a, b) => a - b },
])

let b = sortByMany([16, 22, 34, 73], [
    { get: e => e % 10, cmp: (a, b) => a - b },
    // doesn't typecheck, no idea why
    // `cpm`'s type is correct, but arg types don't infer by whatever reason
    { get: e => e % 10 + '', cmp: (a, b) => a - b },
])
Dimava
  • 7,654
  • 1
  • 9
  • 24
  • Great solution! Better approach if ( A extends any[] ) is modified to ( A extends T[keyof T][] ) But as you write in the example B, I think Typescript could not infer the specific key inside the cmp element using only arrays. The next response of vighnesh153 that uses object can do it. – PacoBlanco Jun 10 '23 at 13:17