Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

grid-sort

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

grid-sort - npm Package Compare versions

Comparing version
1.0.0
to
2.0.0
+20
lib/gridSort.d.ts
export declare type Bucket<T> = T[];
export declare type Buckets<T> = {
[key: number]: Bucket<T>;
};
export declare type Accessor<T extends object> = (item: T) => number;
export declare function getSortedKeys<T>(buckets: Buckets<T>): number[];
export declare function calculateItemsLeft<T>(buckets: Buckets<T>): number;
export declare function rowsAreEqual<T extends object | number>(rowA: T[], rowB: T[], accessor: T extends object ? Accessor<T> : undefined): boolean;
export declare function groupItemsByValue<T extends object | number>(items: T[], accessor: T extends object ? Accessor<T> : undefined): Buckets<T>;
export declare function getFillableItem<T>(buckets: Buckets<T>, maxKey: number): number;
export declare function buildGridFromBuckets<T extends object | number>({ buckets, columns, accessor, }: {
buckets: Buckets<T>;
columns: number;
accessor: T extends object ? Accessor<T> : undefined;
}): T[][];
export declare function gridSort<T extends object | number>({ items, columns, accessor, }: {
items: T[];
columns?: number;
accessor: T extends object ? Accessor<T> : never;
}): T[][];
const keysOf = (object) => Object.keys(object);
const itemsInBucket = (buckets, key) => { var _a; return ((_a = buckets[key]) === null || _a === void 0 ? void 0 : _a.length) || 0 > 0; };
const rowQuantity = (row, accessor) => row.reduce((acc, item) => {
if (accessor && typeof item === "object") {
const value = accessor(item);
return acc + value;
}
if (typeof item === "number") {
return acc + item;
}
return acc;
}, 0);
export function getSortedKeys(buckets) {
return keysOf(buckets).sort((a, z) => z - a);
}
export function calculateItemsLeft(buckets) {
return Object.values(buckets).reduce((acc, bucket) => acc + bucket.length, 0);
}
export function rowsAreEqual(rowA, rowB, accessor) {
return rowA.every((value, index) => {
let itemA = value;
let itemB = rowB[index];
if (accessor && typeof itemA === "object" && typeof itemB === "object") {
return accessor(itemA) === accessor(itemB);
}
return itemA === itemB;
});
}
export function groupItemsByValue(items, accessor) {
const groups = {};
for (const item of items) {
let key;
if (accessor && typeof item === "object") {
key = accessor(item);
}
else if (typeof item === "number") {
key = item;
}
if (key === undefined)
continue;
const group = groups[key];
if (group && Array.isArray(group)) {
group.push(item);
}
else {
groups[key] = [item];
}
}
return groups;
}
export function getFillableItem(buckets, maxKey) {
let currentBiggestKey = 0;
const sortedKeys = getSortedKeys(buckets);
for (const key of sortedKeys) {
if (itemsInBucket(buckets, key) &&
key > currentBiggestKey &&
key <= maxKey) {
currentBiggestKey = key;
}
}
return currentBiggestKey;
}
export function buildGridFromBuckets({ buckets, columns, accessor, }) {
const grid = [];
let itemsLeft;
while ((itemsLeft = calculateItemsLeft(buckets)) > 0) {
const row = [];
let key;
while ((key = getFillableItem(buckets, columns - rowQuantity(row, accessor))) &&
rowQuantity(row, accessor) < columns &&
itemsLeft >= 1) {
const bucket = buckets[key];
if (!bucket)
continue;
const item = bucket.shift();
if (!item)
break;
row.push(item);
}
// randomize current row if previous row has same layout
const previousRow = grid[grid.length - 1];
if (previousRow && rowsAreEqual(row, previousRow, accessor)) {
row.reverse();
}
grid.push(row);
}
return grid;
}
export function gridSort({ items, columns = 4, accessor, }) {
const buckets = groupItemsByValue(items, accessor);
return buildGridFromBuckets({ buckets, columns, accessor });
}
export { gridSort } from "./gridSort.js";
export { gridSort } from "./gridSort.js";
export type Bucket<T> = T[];
export type Buckets<T> = { [key: number]: Bucket<T> };
export type Accessor<T extends object> = (item: T) => number;
const keysOf = <T extends object>(object: T) =>
Object.keys(object) as (keyof typeof object)[];
const itemsInBucket = <T extends unknown>(
buckets: Buckets<T>,
key: keyof Buckets<T>
) => buckets[key]?.length || 0 > 0;
const rowQuantity = <T extends object | number>(
row: T[],
accessor: T extends object ? Accessor<T> : undefined
) =>
row.reduce((acc, item) => {
if (accessor && typeof item === "object") {
const value = accessor(item);
return acc + value;
}
if (typeof item === "number") {
return acc + item;
}
return acc;
}, 0);
export function getSortedKeys<T>(buckets: Buckets<T>) {
return keysOf(buckets).sort((a, z) => z - a);
}
export function calculateItemsLeft<T>(buckets: Buckets<T>) {
return Object.values(buckets).reduce((acc, bucket) => acc + bucket.length, 0);
}
export function rowsAreEqual<T extends object | number>(
rowA: T[],
rowB: T[],
accessor: T extends object ? Accessor<T> : undefined
) {
return rowA.every((value, index) => {
let itemA = value;
let itemB = rowB[index] as T;
if (accessor && typeof itemA === "object" && typeof itemB === "object") {
return accessor(itemA) === accessor(itemB);
}
return itemA === itemB;
});
}
export function groupItemsByValue<T extends object | number>(
items: T[],
accessor: T extends object ? Accessor<T> : undefined
) {
const groups: Buckets<T> = {};
for (const item of items) {
let key: number | undefined;
if (accessor && typeof item === "object") {
key = accessor(item);
} else if (typeof item === "number") {
key = item;
}
if (key === undefined) continue;
const group = groups[key];
if (group && Array.isArray(group)) {
group.push(item);
} else {
groups[key] = [item];
}
}
return groups;
}
export function getFillableItem<T>(buckets: Buckets<T>, maxKey: number) {
let currentBiggestKey = 0;
const sortedKeys = getSortedKeys(buckets);
for (const key of sortedKeys) {
if (
itemsInBucket(buckets, key) &&
key > currentBiggestKey &&
key <= maxKey
) {
currentBiggestKey = key;
}
}
return currentBiggestKey;
}
export function buildGridFromBuckets<T extends object | number>({
buckets,
columns,
accessor,
}: {
buckets: Buckets<T>;
columns: number;
accessor: T extends object ? Accessor<T> : undefined;
}) {
const grid = [];
let itemsLeft: number;
while ((itemsLeft = calculateItemsLeft(buckets)) > 0) {
const row: T[] = [];
let key: number;
while (
(key = getFillableItem(buckets, columns - rowQuantity(row, accessor))) &&
rowQuantity(row, accessor) < columns &&
itemsLeft >= 1
) {
const bucket = buckets[key];
if (!bucket) continue;
const item = bucket.shift();
if (!item) break;
row.push(item);
}
// randomize current row if previous row has same layout
const previousRow = grid[grid.length - 1];
if (previousRow && rowsAreEqual(row, previousRow, accessor)) {
row.reverse();
}
grid.push(row);
}
return grid;
}
export function gridSort<T extends object | number>({
items,
columns = 4,
accessor,
}: {
items: T[];
columns?: number;
accessor: T extends object ? Accessor<T> : never;
}) {
const buckets = groupItemsByValue(items, accessor);
return buildGridFromBuckets({ buckets, columns, accessor });
}
export { gridSort } from "./gridSort.js";
import { test } from "uvu";
import * as assert from "uvu/assert";
import type { Buckets } from "../lib/gridSort.js";
import {
buildGridFromBuckets,
getFillableItem,
getSortedKeys,
gridSort,
groupItemsByValue,
rowsAreEqual,
} from "../lib/gridSort.js";
test("sorts object keys numerically in descending order", () => {
const buckets = { 1: [1, 1, 1], 2: [2, 2], 3: [3, 3] };
assert.equal(getSortedKeys(buckets), [3, 2, 1].map(String));
});
test("gets best candidate for filling a column", () => {
const bucketsA = { 1: [1, 1, 1], 2: [2, 2], 3: [3, 3] };
assert.equal(getFillableItem(bucketsA, 4), "3");
const bucketsB = { 1: [1, 1, 1], 2: [2, 2], 3: [3, 3] };
assert.equal(getFillableItem(bucketsB, 1), "1");
const bucketsC = { 1: [1, 1, 1], 2: [2, 2], 3: [3, 3] };
assert.equal(getFillableItem(bucketsC, 2), "2");
const bucketsD = { 1: [1, 1, 1], 2: [2, 2], 3: [] };
assert.equal(getFillableItem(bucketsD, 4), "2");
const bucketsE = { 1: [1, 1, 1], 2: [], 3: [] };
assert.equal(getFillableItem(bucketsE, 4), "1");
const bucketsF = { 1: [1, 1, 1], 2: [2, 2], 3: [3, 3] };
assert.equal(getFillableItem(bucketsF, 2), "2");
const bucketsG = { 1: [1, 1, 1], 2: [2, 2], 3: [3, 3] };
assert.equal(getFillableItem(bucketsG, 1), "1");
});
test("rows equality", () => {
const [rowA, rowB] = [
[
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
],
[
{ id: "8", ratio: 3 },
{ id: "5", ratio: 1 },
],
];
const accessor = (item: typeof rowA[number]) => item.ratio;
assert.ok(rowsAreEqual(rowA, rowB, accessor));
assert.not.ok(rowsAreEqual(rowA, rowB.reverse(), accessor));
});
test("it sorts bucket of numbers", () => {
const buckets = {
1: [1, 1, 1, 1, 1],
2: [2, 2],
3: [3, 3],
} as Buckets<number>;
const sorted = buildGridFromBuckets({
buckets,
columns: 4,
accessor: undefined,
});
const expected = [
[3, 1],
[1, 3],
[2, 2],
[1, 1, 1],
];
assert.equal(sorted, expected);
});
test("it builds buckets from array of items", () => {
const items = [
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
{ id: "8", ratio: 3 },
];
const itemsByValue = groupItemsByValue(items, (item) => item.ratio);
assert.equal(itemsByValue, {
1: [
{ id: "2", ratio: 1 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
],
2: [
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
],
3: [
{ id: "1", ratio: 3 },
{ id: "8", ratio: 3 },
],
});
});
test("it sorts bucket of objects", () => {
const items = [
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
{ id: "8", ratio: 3 },
];
const accessor = (item: typeof items[number]) => item.ratio;
const itemsByValue = groupItemsByValue(items, accessor);
const sorted = buildGridFromBuckets({
buckets: itemsByValue,
columns: 4,
accessor,
});
const expected = [
[
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
],
[
{ id: "5", ratio: 1 },
{ id: "8", ratio: 3 },
],
[
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
],
[{ id: "7", ratio: 1 }],
];
assert.equal(sorted, expected);
});
test("it sorts bucket of objects with different columns", () => {
const items = [
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
{ id: "8", ratio: 3 },
];
const accessor = (item: typeof items[number]) => item.ratio;
const itemsByValue = groupItemsByValue(items, accessor);
const sorted = buildGridFromBuckets({
buckets: itemsByValue,
columns: 5,
accessor,
});
const expected = [
[
{ id: "1", ratio: 3 },
{ id: "3", ratio: 2 },
],
[
{ id: "4", ratio: 2 },
{ id: "8", ratio: 3 },
],
[
{ id: "2", ratio: 1 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
],
];
assert.equal(sorted, expected);
});
test("it sorts an array of items into a grid", () => {
const items = [
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
{ id: "8", ratio: 3 },
];
const accessor = (item: typeof items[number]) => item.ratio;
const sorted = gridSort({
items,
columns: 5,
accessor,
});
const expected = [
[
{ id: "1", ratio: 3 },
{ id: "3", ratio: 2 },
],
[
{ id: "4", ratio: 2 },
{ id: "8", ratio: 3 },
],
[
{ id: "2", ratio: 1 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
],
];
assert.equal(sorted, expected);
});
test.run();
{
"compilerOptions": {
"declaration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ESNext"],
"module": "ES2020",
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "./lib",
"strict": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"skipLibCheck": true,
"target": "ES2015",
},
"include": ["src"]
}
+9
-3
{
"name": "grid-sort",
"version": "1.0.0",
"version": "2.0.0",
"description": "Sort objects in a two dimensional array to compose grids based on a condition",
"main": "index.js",
"exports": "./lib/index.js",
"author": "Luis Adame Rodríguez <luis@adame.dev>",

@@ -10,7 +10,13 @@ "license": "MIT",

"devDependencies": {
"ts-node": "^10.2.1",
"typescript": "^4.3.5",
"uvu": "^0.5.1"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"test": "node tests"
"test": "node --experimental-loader ts-node/esm node_modules/uvu/bin.js tests",
"tsc": "tsc"
}
}
export const getSortedKeys = (buckets) =>
Object.keys(buckets).sort((a, z) => z - a);
const calculateItemsLeft = (buckets) =>
Object.values(buckets).reduce((acc, bucket) => acc + bucket.length, 0);
const itemsInBucket = (buckets, key) => buckets[key].length > 0;
const rowQuantity = (row, accessor) =>
row.reduce((acc, item) => {
const value = accessor ? accessor(item) : item;
return acc + value;
}, 0);
export const rowsAreEqual = (rowA, rowB, accessor) =>
rowA.every((value, index) => {
let itemA = value;
let itemB = rowB[index];
if (accessor) {
itemA = accessor(itemA);
itemB = accessor(itemB);
}
return itemA === itemB;
});
/**
* @param {Array<*>} items
* @param {(item) => * | undefined} accessor
*/
export function groupItemsByValue(items, accessor) {
const groups = {};
for (const item of items) {
let key = item;
if (accessor) {
key = accessor(item);
}
groups[key] = groups[key] ? [...groups[key], item] : [item];
}
return groups;
}
export const getFillableItem = (buckets, maxKey) => {
let currentBiggestKey = 0;
const sortedKeys = getSortedKeys(buckets);
for (const key of sortedKeys) {
if (
itemsInBucket(buckets, key) &&
key > currentBiggestKey &&
key <= maxKey
) {
currentBiggestKey = key;
}
}
return currentBiggestKey;
};
/**
*
* @param {Object<number, Array<*>>} buckets
* @param {Number} columns
* @param {(item) => *} accessor
*/
export function makeGrid(buckets, columns = 4, accessor) {
const grid = [];
let itemsLeft;
while ((itemsLeft = calculateItemsLeft(buckets)) > 0) {
const row = [];
let key;
while (
(key = getFillableItem(buckets, columns - rowQuantity(row, accessor))) &&
rowQuantity(row, accessor) < columns &&
itemsLeft >= 1
) {
row.push(buckets[key].shift());
}
// randomize current row if previous row has same layout
const previousRow = grid[grid.length - 1];
if (previousRow && rowsAreEqual(row, previousRow, accessor)) {
row.reverse();
}
grid.push(row);
}
return grid;
}
import { test } from "uvu";
import * as assert from "uvu/assert";
import {
getFillableItem,
getSortedKeys,
groupItemsByValue,
makeGrid,
rowsAreEqual,
} from "../index.js";
test("getSortedKeys", () => {
const buckets = { 1: [1, 1, 1], 2: [2, 2], 3: [3, 3] };
assert.equal(getSortedKeys(buckets), [3, 2, 1].map(String));
});
test("getFillableItem", () => {
const buckets = { 1: [1, 1, 1], 2: [2, 2], 3: [3, 3] };
assert.equal(getFillableItem(buckets, 4), "3");
});
test("rowsAreEqual", () => {
let rowA = [
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
];
let rowB = [
{ id: "8", ratio: 3 },
{ id: "5", ratio: 1 },
];
const accessor = (item) => item.ratio;
assert.ok(rowsAreEqual(rowA, rowB, accessor));
rowA = [
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
];
rowB = [
{ id: "5", ratio: 1 },
{ id: "8", ratio: 3 },
];
assert.not.ok(rowsAreEqual(rowA, rowB, accessor));
});
test("it sorts bucket of numbers", () => {
const buckets = { 1: [1, 1, 1, 1, 1], 2: [2, 2], 3: [3, 3] };
const sorted = makeGrid(buckets);
const expected = [
[3, 1],
[1, 3],
[2, 2],
[1, 1, 1],
];
assert.equal(sorted, expected);
});
test("groupItemsByValue", () => {
const items = [
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
{ id: "8", ratio: 3 },
];
const itemsByValue = groupItemsByValue(items, (item) => item.ratio);
assert.equal(itemsByValue, {
1: [
{ id: "2", ratio: 1 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
],
2: [
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
],
3: [
{ id: "1", ratio: 3 },
{ id: "8", ratio: 3 },
],
});
});
test("it sorts bucket of objects", () => {
const items = [
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
{ id: "8", ratio: 3 },
];
const accessor = (item) => item.ratio;
const itemsByValue = groupItemsByValue(items, accessor);
const sorted = makeGrid(itemsByValue, 4, accessor);
const expected = [
[
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
],
[
{ id: "5", ratio: 1 },
{ id: "8", ratio: 3 },
],
[
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
],
[{ id: "7", ratio: 1 }],
];
assert.equal(sorted, expected);
});
test("it sorts bucket of objects with different columns", () => {
const items = [
{ id: "1", ratio: 3 },
{ id: "2", ratio: 1 },
{ id: "3", ratio: 2 },
{ id: "4", ratio: 2 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
{ id: "8", ratio: 3 },
];
const accessor = (item) => item.ratio;
const itemsByValue = groupItemsByValue(items, accessor);
const sorted = makeGrid(itemsByValue, 5, accessor);
const expected = [
[
{ id: "1", ratio: 3 },
{ id: "3", ratio: 2 },
],
[
{ id: "4", ratio: 2 },
{ id: "8", ratio: 3 },
],
[
{ id: "2", ratio: 1 },
{ id: "5", ratio: 1 },
{ id: "7", ratio: 1 },
],
];
assert.equal(sorted, expected);
});
test.run();