diff --git a/package.json b/package.json index 3889322..c16f1a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "csv-picker", - "version": "1.0.0", + "version": "0.2.0", "description": "", "main": "dist/src/index.js", "module": "dist/src/index.esm.js", diff --git a/src/helpers/sort.test.ts b/src/helpers/sort.test.ts index 31e54bd..f3cbe41 100644 --- a/src/helpers/sort.test.ts +++ b/src/helpers/sort.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { swap, defaultCompare, sortProducts, ProductTuple } from "./sort"; +import { swap, ProductTuple, sortProducts, compareStrings, Compare } from "./sort"; describe("swap", () => { it("should swap two items in an array", () => { @@ -9,61 +9,43 @@ describe("swap", () => { }); }); -describe("defaultCompare", () => { - it("should return -1 if a < b", () => { - const result = defaultCompare(1, 2); - expect(result).toBe(-1); - }); - it("should return 1 if a > b", () => { - const result = defaultCompare(2, 1); - expect(result).toBe(1); - }); - it("should return 0 if a === b", () => { - const result = defaultCompare(1, 1); - expect(result).toBe(0); - }); -}); - describe("array sort", () => { - it("should sort an array of numbers", () => { - const array = [4, 2, 3, 1, 5]; - const result = array.sort((a, b) => defaultCompare(a, b)); - expect(result).toEqual([1, 2, 3, 4, 5]); - }); - it("should sort an array of strings", () => { const array = ["a", "z", "ag", "ac", "ab", "ae", "ba"]; - const result = array.sort((a, b) => defaultCompare(a, b)); - expect(result).toEqual(["a", "ab", "ac", "ae", "ag", "ba", "z"]); + const result = array.sort((a, b) => compareStrings(a, b)); + expect(result).toEqual(["a", "z", "ab", "ac", "ae", "ag", "ba"]); }); + it("should sort an array of strings that are numbers", () => { const array = ["5", "4", "3", "2", "1"]; - const result = array.sort((a, b) => defaultCompare(a, b)); + const result = array.sort((a, b) => compareStrings(a, b)); expect(result).toEqual(["1", "2", "3", "4", "5"]); }); }); describe("sortProducts", () => { - it("should sort an array of products by pick location", () => { + it("should sort an array of products by pick location in ascending order from A 1 to A 10", () => { const data: ProductTuple[] = [ ["product_code", "quantity", "pick_location"], - ["B1234", "2", "A3"], - ["B1235", "3", "A2"], - ["B1236", "4", "A1"] + ["B1237", "2", "A 10"], + ["B1234", "2", "A 3"], + ["B1235", "3", "A 2"], + ["B1236", "4", "A 1"] ]; const [columns, ...rows] = data; - const sortedRows = sortProducts(rows); + const sortedRows = sortProducts(rows, "ascending"); const result = [columns, ...sortedRows]; expect(result).toEqual([ ["product_code", "quantity", "pick_location"], - ["B1236", "4", "A1"], - ["B1235", "3", "A2"], - ["B1234", "2", "A3"] + ["B1236", "4", "A 1"], + ["B1235", "3", "A 2"], + ["B1234", "2", "A 3"], + ["B1237", "2", "A 10"] ]); }); - it("should sort an array of products by bay and shelf height in ascending order", () => { + it("should sort an array of products by pick location (bay and shelf height) in ascending order", () => { const data: ProductTuple[] = [ ["product_code", "quantity", "pick_location"], ["A", "10", "Z 1"], @@ -73,7 +55,7 @@ describe("sortProducts", () => { ]; const [columns, ...rows] = data; - const sortedRows = sortProducts(rows); + const sortedRows = sortProducts(rows, "ascending"); const result = [columns, ...sortedRows]; expect(result).toEqual([ @@ -90,19 +72,245 @@ describe("sortProducts", () => { ["E", "1", "AB 1"], ["F", "1", "AB 10"], ["H", "1", "AB 9"], - ["H", "1", "AB 7"] + ["J", "1", "AB 7"] ]; const [columns, ...rows] = data; - const sortedRows = sortProducts(rows); + const sortedRows = sortProducts(rows, "ascending"); const result = [columns, ...sortedRows]; expect(result).toEqual([ ["product_code", "quantity", "pick_location"], ["E", "1", "AB 1"], - ["H", "1", "AB 7"], + ["J", "1", "AB 7"], ["H", "1", "AB 9"], ["F", "1", "AB 10"] ]); }); + + it("should sort by shelf height if the bays are not the same", () => { + const data: ProductTuple[] = [ + ["product_code", "quantity", "pick_location"], + ["B1237", "2", "A 10"], + ["B1237", "2", "A 10"], + ["B12311", "2", "Z 10"], + ["B1234", "2", "Z 3"], + ["B1235", "2", "A 3"], + ["B1236", "3", "Z 2"], + ["B1238", "4", "Z 1"], + ["B1239", "3", "A 2"], + ["B12310", "4", "A 1"] + ]; + + const [columns, ...rows] = data; + + const sortedRows = sortProducts(rows, "ascending"); + const result = [columns, ...sortedRows]; + expect(result).toEqual([ + ["product_code", "quantity", "pick_location"], + ["B12310", "4", "A 1"], + ["B1239", "3", "A 2"], + ["B1235", "2", "A 3"], + ["B1237", "4", "A 10"], + ["B1238", "4", "Z 1"], + ["B1236", "3", "Z 2"], + ["B1234", "2", "Z 3"], + ["B12311", "2", "Z 10"] + ]); + }); +}); + +describe("sortProducts", () => { + const data: ProductTuple[] = [ + ["product_code", "quantity", "pick_location"], + ["1", "1", "AB 1"], + ["2", "1", "AB 10"], + ["3", "1", "AB 9"], + ["4", "1", "AB 7"] + ]; + + it("should sort products with the same bay by shelf height", () => { + const [, ...rows] = data; + const result = sortProducts(rows, "ascending"); + expect(result).toEqual([ + ["1", "1", "AB 1"], + ["4", "1", "AB 7"], + ["3", "1", "AB 9"], + ["2", "1", "AB 10"] + ]); + }); + + it('should reverse the results if the method is "descending"', () => { + const [, ...rows] = data; + const result = sortProducts(rows, "descending"); + expect(result).toEqual([ + ["2", "1", "AB 10"], + ["3", "1", "AB 9"], + ["4", "1", "AB 7"], + ["1", "1", "AB 1"] + ]); + }); + + it("should deduplicate products by id and add their qty", () => { + const data: ProductTuple[] = [ + ["product_code", "quantity", "pick_location"], + ["1", "1", "AB 1"], + ["1", "1", "AB 1"], + ["2", "1", "AB 10"], + ["3", "1", "AB 9"], + ["4", "1", "AB 7"] + ]; + const [, ...rows] = data; + const result = sortProducts(rows, "descending"); + expect(result).toEqual([ + ["2", "1", "AB 10"], + ["3", "1", "AB 9"], + ["4", "1", "AB 7"], + ["1", "2", "AB 1"] + ]); + }); + + it("should sort products that do not have the same bay by bay and then by shelf height", () => { + const data: ProductTuple[] = [ + ["product_code", "quantity", "pick_location"], + ["3", "1", "AB 10"], + ["4", "1", "AB 9"], + ["5", "1", "AB 7"], + ["6", "1", "AZ 1"], + ["7", "1", "AZ 10"], + ["8", "1", "AZ 9"], + ["9", "1", "AZ 7"] + ]; + const [, ...rows] = data; + const result = sortProducts(rows, "ascending"); + expect(result).toEqual([ + ["5", "1", "AB 7"], + ["4", "1", "AB 9"], + ["3", "1", "AB 10"], + ["6", "1", "AZ 1"], + ["9", "1", "AZ 7"], + ["8", "1", "AZ 9"], + ["7", "1", "AZ 10"] + ]); + }); + + it("should sort an array of products by pick location in ascending order from A 1 to AZ 10", () => { + const data: ProductTuple[] = [ + ["product_code", "quantity", "pick_location"], + ["1", "1", "A 1"], + ["2", "1", "Z 1"], + ["3", "1", "AB 10"], + ["9", "1", "AZ 7"] + ]; + const [, ...rows] = data; + const result = sortProducts(rows, "ascending"); + expect(result).toEqual([ + ["1", "1", "A 1"], + ["2", "1", "Z 1"], + ["3", "1", "AB 10"], + ["9", "1", "AZ 7"] + ]); + }); +}); + +describe("compareStrings", () => { + it("should throw an error if the string is more than two characters", () => { + expect(() => compareStrings("ABC", "ABC")).toThrowError(); + expect(() => compareStrings("ABC", "AB")).toThrowError(); + expect(() => compareStrings("AB", "ABC")).toThrowError(); + expect(() => compareStrings("AB", "AB")).not.toThrowError(); + }); + describe("single char", () => { + // Case 1: A, A - single chars, both match + it("should return EQUALS if single chars, both match", () => { + const data = ["A", "A"]; + const result = compareStrings(data[0], data[1]); + expect(result).toEqual(Compare.EQUALS); + }); + + // Case 2: A, Z || Z, A - single chars, no match + it("should return BIGGER THAN if single chars, no match", () => { + const data = ["Z", "A"]; + const result = compareStrings(data[0], data[1]); + expect(result).toEqual(Compare.BIGGER_THAN); + }); + + it("should return LESS THAN if single chars, no match", () => { + const data = ["A", "Z"]; + const result = compareStrings(data[0], data[1]); + expect(result).toEqual(Compare.LESS_THAN); + }); + }); + + describe("multiple chars", () => { + // Case 3: AA, AA - multiple chars, both match + it("should return EQUALS if the first and second letters are the same", () => { + const data = ["AA", "AA"]; + const result = compareStrings(data[0], data[1]); + expect(result).toEqual(Compare.EQUALS); + }); + + // Case 4: AA, AZ - multiple chars, only first letters match + it("should return LESS THAN if the first letters match but the second char is lower", () => { + const data = ["AA", "AB"]; + const result = compareStrings(data[0], data[1]); + expect(result).toEqual(Compare.LESS_THAN); + }); + + it("should return BIGGER THAN if the first letters match but the second char is higher", () => { + const data = ["AB", "AA"]; + const result = compareStrings(data[0], data[1]); + expect(result).toEqual(Compare.BIGGER_THAN); + }); + + // Case 5: ZA, AA - multiple chars, only second letters match + it("should return LESS THAN if the first letters dont match and only second letters match", () => { + const data = ["AA", "BA"]; + const result = compareStrings(data[0], data[1]); + expect(result).toEqual(Compare.LESS_THAN); + }); + + it("should return BIGGER THAN if the first letters dont match and only second letters match", () => { + const data = ["BA", "AA"]; + const result = compareStrings(data[0], data[1]); + expect(result).toEqual(Compare.BIGGER_THAN); + }); + + // Case 6: ZA, AZ - multiple chars, no match + it("should return BIGGER THAN if no chars match and the first char is lower", () => { + const data = [ + ["ZA", "AZ"], + ["XY", "AK"] + ]; + data.forEach(([a, b]) => { + const result = compareStrings(a, b); + expect(result).toEqual(Compare.BIGGER_THAN); + }); + }); + + it("should return LESS THAN if no chars match and the first char is higher", () => { + const data = [ + ["AZ", "ZA"], + ["KA", "XY"] + ]; + data.forEach(([a, b]) => { + const result = compareStrings(a, b); + expect(result).toEqual(Compare.LESS_THAN); + }); + }); + + // Case 7: AA, Z - multiple chars, and single char + it("should return BIGGER THAN if the first string is multiple chars and the second is a single char", () => { + const data = ["AA", "Z"]; + const result = compareStrings(data[0], data[1]); + expect(result).toEqual(Compare.BIGGER_THAN); + }); + + // Case 8: Z, AA - single char, and multiple chars + it("should return LESS THAN if the first string is a single char and second is multiple", () => { + const data = ["Z", "AA"]; + const result = compareStrings(data[0], data[1]); + expect(result).toEqual(Compare.LESS_THAN); + }); + }); }); diff --git a/src/helpers/sort.ts b/src/helpers/sort.ts index f7aba00..2b6def0 100644 --- a/src/helpers/sort.ts +++ b/src/helpers/sort.ts @@ -1,4 +1,4 @@ -const Compare = { +export const Compare = { LESS_THAN: -1, BIGGER_THAN: 1, EQUALS: 0 @@ -8,12 +8,12 @@ type Compare = (typeof Compare)[keyof typeof Compare]; export type ProductTuple = [string, string, string]; -export const defaultCompare = (a: string | number, b: string | number): Compare => { - if (a === b) { - return Compare.EQUALS; - } - return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN; -}; +// export const defaultCompare = (a: string | number, b: string | number): Compare => { +// if (a === b) { +// return Compare.EQUALS; +// } +// return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN; +// }; export const swap = (array: unknown[], a: number, b: number) => { const temp = array[a]; @@ -21,19 +21,158 @@ export const swap = (array: unknown[], a: number, b: number) => { array[b] = temp; }; -export const sortProducts = (products: ProductTuple[]) => { - const sortByPickLocation = (a: ProductTuple, b: ProductTuple) => { - const [, , pickLocationA] = a; - const [, , pickLocationB] = b; - const [bayA, shelfA] = pickLocationA.split(" "); - const [bayB, shelfB] = pickLocationB.split(" "); +// export const sortProducts = (products: ProductTuple[]) => { +// const sortByPickLocation = (a: ProductTuple, b: ProductTuple) => { +// const [, , pickLocationA] = a; +// const [, , pickLocationB] = b; +// const [bayA, shelfA] = pickLocationA.split(" "); +// const [bayB, shelfB] = pickLocationB.split(" "); + +// if (bayA === bayB) { +// return defaultCompare(Number(shelfA), Number(shelfB)); +// } + +// return defaultCompare(pickLocationA, pickLocationB); +// }; +// products.sort(sortByPickLocation); +// return products; +// }; + +type SortMethod = "ascending" | "descending"; + +const filterProductsById = (products: ProductTuple[]) => { + const productsMap = new Map(); - if (bayA === bayB) { - return defaultCompare(Number(shelfA), Number(shelfB)); + for (let p = 0; p < products.length; p++) { + const product = products[p]; + const [id] = product; + + if (productsMap.has(id)) { + const existingProduct = productsMap.get(id); + + if (existingProduct?.length === 3) { + const [, existingQty] = existingProduct; + const [, qty] = product; + existingProduct[1] = String(Number(existingQty) + Number(qty)); + productsMap.set(id, existingProduct); + } + continue; } + productsMap.set(id, product); + } + + return [...productsMap.values()]; +}; + +export const compareStrings = (a: string, b: string) => { + // Always comparing a to b + + // Single chars + // Case 1: A, A - single chars, both match + // Case 2: A, Z || Z, A - single chars, no match + + // Multiple chars + // Case 3: AA, AA - multiple chars, both match + // Case 4: AA, AZ - multiple chars, only first letters match + // Case 5: ZA, AA - multiple chars, only second letters match + // Case 6: ZA, AZ - multiple chars, no match + + // Both + // Case 7: AA, Z - multiple chars, and single char + // Case 8: Z, AA - single char, and multiple chars + + if (a.length > 2 || b.length > 2) { + throw new Error("Invalid string length; string must be 2 characters or less."); + } + + if (a.length === 1 && b.length === 1) { + // Case 1: A, A - single chars, both match + if (a === b) { + return Compare.EQUALS; + } + // Case 2: A, Z || Z, A - single chars, no match + return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN; + } + + if (a.length > 1 && b.length > 1) { + const [firstLetterA, secondLetterA] = a.split(""); + const [firstLetterB, secondLetterB] = b.split(""); + + if (firstLetterA === firstLetterB) { + if (secondLetterA === secondLetterB) { + // Case 3: AA, AA - multiple chars, both match + return Compare.EQUALS; + } + + // Case 4: AA, AZ - multiple chars, only first letters match + return secondLetterA < secondLetterB ? Compare.LESS_THAN : Compare.BIGGER_THAN; + } + + // Case 6: ZA, AZ - multiple chars, no match + if (firstLetterA !== firstLetterB && secondLetterA !== secondLetterB) { + return firstLetterA < firstLetterB ? Compare.LESS_THAN : Compare.BIGGER_THAN; + } + + // Case 5: ZA, AA - multiple chars, only second letters match + return firstLetterA < firstLetterB ? Compare.LESS_THAN : Compare.BIGGER_THAN; + } + + // Case 7: AA, Z - multiple chars, and single char + if (a.length > 1 && b.length === 1) { + return Compare.BIGGER_THAN; + } + + // Case 8: Z, AA - single char, and multiple chars + // if (a.length === 1 && b.length > 1) { + return Compare.LESS_THAN; + // } +}; + +export const sortProducts = (products: ProductTuple[], method: SortMethod) => { + const result = filterProductsById(products); + + for (let p = 0; p < products.length; p++) { + // iterate over products + + for (let i = 0; i < result.length; i++) { + // sort each product by pick location + const currentProduct = result[i]; + const nextProduct = result[i + 1]; + + if (!nextProduct) { + break; + } + + const [, , currentPickLocation] = currentProduct; + const [, , nextPickLocation] = nextProduct; + const [currentBay, currentShelf] = currentPickLocation.split(" "); + const [nextBay, nextShelf] = nextPickLocation.split(" "); + + const compare = compareStrings(currentBay, nextBay); + + // top of the sort order + // Case 1: skip all less than results + if (compare === Compare.LESS_THAN) { + continue; + } + + // Case 2: swap based on shelf if both match + if (compare === Compare.EQUALS) { + if (Number(currentShelf) > Number(nextShelf)) { + swap(result, i, i + 1); + } + } + + // Case 3: swap based on bay + if (compare === Compare.BIGGER_THAN) { + swap(result, i, i + 1); + } + } + } + + if (method === "descending") { + result.reverse(); + } - return defaultCompare(pickLocationA, pickLocationB); - }; - products.sort(sortByPickLocation); - return products; + return result; }; diff --git a/src/index.ts b/src/index.ts index 4b5e30d..0d943bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,9 +39,9 @@ async function run(args: string[], options: CliOptions = {}) { checkColumns(columns); logger.info(`processing ${rows.length} rows`); - sortProducts(rows); + const sortedRows = sortProducts(rows, "ascending"); - const sortedProducts = [columns, ...rows]; + const sortedProducts = [columns, ...sortedRows]; logger.success(`sorted ${sortedProducts.length - 1} products`); createCsvFile(options.output, sortedProducts); diff --git a/src/output.csv b/src/output.csv index 600164e..c6c7373 100644 --- a/src/output.csv +++ b/src/output.csv @@ -1,9 +1,12 @@ product_code,quantity,pick_location 25214,10,A 1 30124,5,A 1 +25636,1,C 8 +15178,9,D 4 +12345,15,L 3 +23689,10,X 10 12456,10,AB 9 -15248,10,AB 10 -15248,5,AB 10 +15248,15,AB 10 52568,7,AB 10 33331,6,AC 4 36389,4,AC 5 @@ -13,8 +16,4 @@ product_code,quantity,pick_location 12879,12,AL 7 14789,3,AM 9 11224,8,AZ 4 -88958,4,AZ 10 -25636,1,C 8 -15178,9,D 4 -12345,15,L 3 -23689,10,X 10 \ No newline at end of file +88958,4,AZ 10 \ No newline at end of file