diff --git a/collections/deno.json b/collections/deno.json index f874ed03c..3c57e0e47 100644 --- a/collections/deno.json +++ b/collections/deno.json @@ -46,6 +46,7 @@ "./take-last-while": "./take_last_while.ts", "./take-while": "./take_while.ts", "./union": "./union.ts", + "./unstable-sort-by": "./unstable_sort_by.ts", "./unstable-take-while": "./unstable_take_while.ts", "./unzip": "./unzip.ts", "./without-all": "./without_all.ts", diff --git a/collections/sort_by.ts b/collections/sort_by.ts index 1e5b29bdc..84761facf 100644 --- a/collections/sort_by.ts +++ b/collections/sort_by.ts @@ -20,6 +20,9 @@ export type SortByOptions = { * element. Ascending or descending order can be specified through the `order` * option. By default, the elements are sorted in ascending order. * + * Note: If you want to process any iterable, use the new version of + * `sortBy` from `@std/collections/unstable-sort-by`. + * * @typeParam T The type of the array elements. * * @param array The array to sort. diff --git a/collections/sort_by_test.ts b/collections/sort_by_test.ts index 5abb8f5df..01d19725c 100644 --- a/collections/sort_by_test.ts +++ b/collections/sort_by_test.ts @@ -2,6 +2,7 @@ import { assertEquals } from "@std/assert"; import { sortBy } from "./sort_by.ts"; +import { sortBy as unstableSortBy } from "./unstable_sort_by.ts"; Deno.test({ name: "sortBy() handles no mutation", @@ -221,3 +222,255 @@ Deno.test({ ); }, }); + +Deno.test({ + name: "(unstable) sortBy() handles no mutation", + fn() { + const array = ["a", "abc", "ba"]; + unstableSortBy(array, (it) => it.length); + + assertEquals(array, ["a", "abc", "ba"]); + }, +}); + +Deno.test({ + name: "(unstable) sortBy() calls the selector function once", + fn() { + let callCount = 0; + const array = [0, 1, 2]; + unstableSortBy(array, (it) => { + callCount++; + return it; + }); + + assertEquals(callCount, array.length); + }, +}); + +Deno.test({ + name: "(unstable) sortBy() handles empty input", + fn() { + assertEquals(unstableSortBy([], () => 5), []); + }, +}); + +Deno.test({ + name: "(unstable) sortBy() handles identity selector", + fn() { + assertEquals(unstableSortBy([2, 3, 1], (it) => it), [1, 2, 3]); + }, +}); + +Deno.test({ + name: "(unstable) sortBy() handles stable sort", + fn() { + assertEquals( + unstableSortBy([ + { id: 1, date: "February 1, 2022" }, + { id: 2, date: "December 17, 1995" }, + { id: 3, date: "June 12, 2012" }, + { id: 4, date: "December 17, 1995" }, + { id: 5, date: "June 12, 2012" }, + ], (it) => new Date(it.date)), + [ + { id: 2, date: "December 17, 1995" }, + { id: 4, date: "December 17, 1995" }, + { id: 3, date: "June 12, 2012" }, + { id: 5, date: "June 12, 2012" }, + { id: 1, date: "February 1, 2022" }, + ], + ); + + assertEquals( + unstableSortBy([ + { id: 1, str: "c" }, + { id: 2, str: "a" }, + { id: 3, str: "b" }, + { id: 4, str: "a" }, + { id: 5, str: "b" }, + ], (it) => it.str), + [ + { id: 2, str: "a" }, + { id: 4, str: "a" }, + { id: 3, str: "b" }, + { id: 5, str: "b" }, + { id: 1, str: "c" }, + ], + ); + }, +}); + +Deno.test({ + name: "(unstable) sortBy() handles special number values", + fn() { + assertEquals( + unstableSortBy([ + 1, + Number.POSITIVE_INFINITY, + 2, + Number.NEGATIVE_INFINITY, + 3, + Number.NaN, + 4, + Number.NaN, + ], (it) => it), + [ + Number.NEGATIVE_INFINITY, + 1, + 2, + 3, + 4, + Number.POSITIVE_INFINITY, + Number.NaN, + Number.NaN, + ], + ); + + assertEquals( + unstableSortBy([ + Number.NaN, + 1, + Number.POSITIVE_INFINITY, + Number.NaN, + 7, + Number.NEGATIVE_INFINITY, + Number.NaN, + 2, + 6, + 5, + 9, + ], (it) => it), + [ + Number.NEGATIVE_INFINITY, + 1, + 2, + 5, + 6, + 7, + 9, + Number.POSITIVE_INFINITY, + Number.NaN, + Number.NaN, + Number.NaN, + ], + ); + + // Test that NaN sort is stable. + const nanArray = [ + { id: 1, nan: Number.NaN }, + { id: 2, nan: Number.NaN }, + { id: 3, nan: Number.NaN }, + { id: 4, nan: Number.NaN }, + ]; + assertEquals(unstableSortBy(nanArray, ({ nan }) => nan), nanArray); + }, +}); + +Deno.test({ + name: "(unstable) sortBy() handles sortings", + fn() { + const testArray = [ + { name: "benchmark", stage: 3 }, + { name: "test", stage: 2 }, + { name: "build", stage: 1 }, + { name: "deploy", stage: 4 }, + ]; + + assertEquals(unstableSortBy(testArray, (it) => it.stage), [ + { name: "build", stage: 1 }, + { name: "test", stage: 2 }, + { name: "benchmark", stage: 3 }, + { name: "deploy", stage: 4 }, + ]); + + assertEquals(unstableSortBy(testArray, (it) => it.name), [ + { name: "benchmark", stage: 3 }, + { name: "build", stage: 1 }, + { name: "deploy", stage: 4 }, + { name: "test", stage: 2 }, + ]); + + assertEquals( + unstableSortBy([ + "9007199254740999", + "9007199254740991", + "9007199254740995", + ], (it) => BigInt(it)), + [ + "9007199254740991", + "9007199254740995", + "9007199254740999", + ], + ); + + assertEquals( + unstableSortBy([ + "February 1, 2022", + "December 17, 1995", + "June 12, 2012", + ], (it) => new Date(it)), + [ + "December 17, 1995", + "June 12, 2012", + "February 1, 2022", + ], + ); + }, +}); + +Deno.test({ + name: "(unstable) sortBy() handles desc ordering", + fn() { + assertEquals( + unstableSortBy( + [ + "January 27, 1995", + "November 26, 2020", + "June 17, 1952", + "July 15, 1993", + ], + (it) => new Date(it), + { order: "desc" }, + ), + [ + "November 26, 2020", + "January 27, 1995", + "July 15, 1993", + "June 17, 1952", + ], + ); + }, +}); + +Deno.test({ + name: "(unstable) sortBy() works with iterators", + fn() { + const set = new Set([10, 312, 99, 5.45, 100, -3, 4.6]); + + assertEquals( + unstableSortBy(set, (it) => it), + [-3, 4.6, 5.45, 10, 99, 100, 312], + ); + assertEquals( + unstableSortBy(set, (it) => it, { order: "desc" }), + [312, 100, 99, 10, 5.45, 4.6, -3], + ); + + const map = new Map([ + ["a", 2], + ["c", 1], + ["b", 3], + ]); + + assertEquals(unstableSortBy(map, (it) => it[0]), [ + ["a", 2], + ["b", 3], + ["c", 1], + ]); + assertEquals(unstableSortBy(map, (it) => it[1], { order: "desc" }), [ + ["b", 3], + ["a", 2], + ["c", 1], + ]); + }, +}); diff --git a/collections/unstable_sort_by.ts b/collections/unstable_sort_by.ts new file mode 100644 index 000000000..b399d7d02 --- /dev/null +++ b/collections/unstable_sort_by.ts @@ -0,0 +1,149 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// This module is browser compatible. + +/** Order option for {@linkcode SortByOptions}. */ +export type Order = "asc" | "desc"; + +/** Options for {@linkcode sortBy}. */ +export type SortByOptions = { + /** + * The order to sort the elements in. + * + * @default {"asc"} + */ + order: Order; +}; + +/** + * Types that can be compared with other values of the same type + * using comparison operators. + */ +export type Comparable = number | string | bigint | Date; + +/** + * Returns all elements in the given collection, sorted by their result using + * the given selector. The selector function is called only once for each + * element. Ascending or descending order can be specified through the `order` + * option. By default, the elements are sorted in ascending order. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @typeParam T The type of the iterator elements. + * @typeParam U The type of the selected values. + * + * @param iterator The iterator to sort. + * @param selector The selector function to get the value to sort by. + * @param options The options for sorting. + * + * @returns A new array containing all elements sorted by the selector. + * + * @example Usage with numbers + * ```ts + * import { sortBy } from "@std/collections/sort-by"; + * import { assertEquals } from "@std/assert"; + * + * const people = [ + * { name: "Anna", age: 34 }, + * { name: "Kim", age: 42 }, + * { name: "John", age: 23 }, + * ]; + * const sortedByAge = sortBy(people, (person) => person.age); + * + * assertEquals(sortedByAge, [ + * { name: "John", age: 23 }, + * { name: "Anna", age: 34 }, + * { name: "Kim", age: 42 }, + * ]); + * + * const sortedByAgeDesc = sortBy(people, (person) => person.age, { order: "desc" }); + * + * assertEquals(sortedByAgeDesc, [ + * { name: "Kim", age: 42 }, + * { name: "Anna", age: 34 }, + * { name: "John", age: 23 }, + * ]); + * ``` + * + * @example Usage with strings + * ```ts + * import { sortBy } from "@std/collections/sort-by"; + * import { assertEquals } from "@std/assert"; + * + * const people = [ + * { name: "Anna" }, + * { name: "Kim" }, + * { name: "John" }, + * ]; + * const sortedByName = sortBy(people, (it) => it.name); + * + * assertEquals(sortedByName, [ + * { name: "Anna" }, + * { name: "John" }, + * { name: "Kim" }, + * ]); + * ``` + * + * @example Usage with bigints + * ```ts + * import { sortBy } from "@std/collections/sort-by"; + * import { assertEquals } from "@std/assert"; + * + * const people = [ + * { name: "Anna", age: 34n }, + * { name: "Kim", age: 42n }, + * { name: "John", age: 23n }, + * ]; + * + * const sortedByAge = sortBy(people, (person) => person.age); + * + * assertEquals(sortedByAge, [ + * { name: "John", age: 23n }, + * { name: "Anna", age: 34n }, + * { name: "Kim", age: 42n }, + * ]); + * ``` + * + * @example Usage with Date objects + * ```ts + * import { sortBy } from "@std/collections/sort-by"; + * import { assertEquals } from "@std/assert"; + * + * const people = [ + * { name: "Anna", startedAt: new Date("2020-01-01") }, + * { name: "Kim", startedAt: new Date("2020-03-01") }, + * { name: "John", startedAt: new Date("2020-06-01") }, + * ]; + * + * const sortedByStartedAt = sortBy(people, (people) => people.startedAt); + * + * assertEquals(sortedByStartedAt, [ + * { name: "Anna", startedAt: new Date("2020-01-01") }, + * { name: "Kim", startedAt: new Date("2020-03-01") }, + * { name: "John", startedAt: new Date("2020-06-01") }, + * ]); + * ``` + */ +export function sortBy( + iterator: Iterable, + selector: (el: T) => U, + options?: SortByOptions, +): T[] { + const array: { value: T; selected: U }[] = []; + + for (const item of iterator) { + array.push({ value: item, selected: selector(item) }); + } + + array.sort((oa, ob) => { + const a = oa.selected; + const b = ob.selected; + const order = options?.order === "desc" ? -1 : 1; + + if (Number.isNaN(a)) return order; + if (Number.isNaN(b)) return -order; + + return order * (a > b ? 1 : a < b ? -1 : 0); + }); + + return array.map((item) => item.value); +}