Marianx
Marianx

Reputation: 5

Javascript Algorithm for inventory

I have a problem, which I solved already but I feel like my implementation is very basic and could definitely learn a better way to do this. Given 2 array of objects, one for the sales that my customers are demanding, and one for the purchases that I'm doing to my provider, I need to be able to place the orders and know when I will be able to satisfy them. I will only work with one product, to make it simpler. I'm very new at coding this case of problems, so I would really apreciate a point in the right direction. Maybe there's a data structure that I haven't used that could help here.

Properties: 'created': when the sales order was created 'quantity': how many items the customer wants

const sales= [{
  'id': 'S1',
  'created': '2020-01-02',
  'quantity': 6
}, {
  'id': 'S2',  
  'created': '2020-11-05',
  'quantity': 2  
}, {
  'id': 'S3',  
  'created': '2019-12-04',
  'quantity': 3  
}, {
  'id': 'S4',  
  'created': '2020-01-20',
  'quantity': 2  
}, {
  'id': 'S5',  
  'created': '2019-12-15',
  'quantity': 9  
}];

Properties: 'receiving': when we expect to receive the product 'quantity': how many we will be receiving

const purchases= [{
  'id': 'P1',  
  'receiving': '2020-01-04',
  'quantity': 4
}, {
  'id': 'P2',  
  'receiving': '2020-01-05',
  'quantity': 3  
}, {
  'id': 'P3',  
  'receiving': '2020-02-01',
  'quantity': 5  
}, {
  'id': 'P4',  
  'receiving': '2020-03-05',
  'quantity': 1  
}, {
  'id': 'P5',  
  'receiving': '2020-02-20',
  'quantity': 7
}];

My code so far. I'm returnign an array that for reach sales, it shows when I will be able to satisfy it. The problem that I'm running with the current implementation is that I cannot cover all the cases.

function allocate(salesOrders, purchaseOrders) {
    //ordering sales and purchases by date
    const orderedSales = salesOrders.sort((a, b) => a.created.localeCompare(b.created));
    const orderedPurchases = purchaseOrders.sort((a, b) => a.receiving.localeCompare(b.receiving));

    console.log(orderedSales)
    console.log(orderedPurchases)
    let stock = 0;
    const result = [];
    purchaseIndex = 0;
    orderedSales.forEach((sale, index) => {
        const order = orderedPurchases[purchaseIndex];
        if (order) {
            console.log("Processing order", sale.id)
            console.log(`Leftover stock = ${stock}`)
            stock += order.quantity
            console.log(`new stock = ${stock}`)
            stock = stock - sale.quantity;
            console.log(`Sustracting = ${sale.quantity}`)
            console.log(`Remaining = ${stock}`)
            while (stock < 0) {
                purchaseIndex++
                console.log(`Demand NOT satified, moving to next purchase order with index ${purchaseIndex}`)

                stock += order.quantity
                console.log(`Current stock = ${stock}`)
                increaseOrder = false;
            }

            //demand has been satisfied
            console.log(`Demand for ${sale.id} was satified with purchase ${order.id}, time is ${order.receiving}, moving to next purchase order`)
            result.push({
                id: sale.id,
                availabilityDate: order.receiving
            })
            purchaseIndex++

            console.log("Next sale ++++++++")
            console.log(" ++++++++")
        }
    });

    console.log(result);
}

allocate(salesOrders, purchaseOrders)

Upvotes: 0

Views: 648

Answers (2)

Scott Sauyet
Scott Sauyet

Reputation: 50807

I would do it like this. First I would create a collection of events that have type, date, id, and quantity fields, then sort them by date. This intermediate format might look like this:

[
  {type: "sale", date: "2019-12-04", id: "S3", quantity: 3}, 
  {type: "sale", date: "2019-12-15", id: "S5", quantity: 9}, 
  {type: "sale", date: "2020-01-02", id: "S1", quantity: 6}, 
  {type: "purchase", date: "2020-01-04", id: "P1", quantity: 4}, 
  {type: "purchase", date: "2020-01-05", id: "P2", quantity: 3}, 
  {type: "sale", date: "2020-01-20", id: "S4", quantity: 2}, 
  {type: "purchase", date: "2020-02-01", id: "P3", quantity: 5}, 
  {type: "purchase", date: "2020-02-20", id: "P5", quantity: 7}, 
  {type: "purchase", date: "2020-03-05", id: "P4", quantity: 1}, 
  {type: "sale", date: "2020-11-05", id: "S2", quantity: 2}
]

Then I would fold this list of events into a structure with onHand, completed and open properties by checking each event. If it's a purchase, then we add its quantity to onHand. Then we loop through the existing open events (plus the current one if it's a sale), creating new entries to add to the existing completed array if the quantity is not bigger than onHand, and adding to a new open array if it's too large. The code could look like this:

const process = (initial) => (sales, purchases) => [
  ...sales.map (({created, ...rest}) => ({type: 'sale', date: created, ...rest})),
  ...purchases.map (({receiving, ...rest}) => ({type: 'purchase', date: receiving, ...rest})),
] .sort (({date: d1}, {date: d2}) => d1 < d2 ? -1 : d1 > d2 ? 1 : 0) .reduce ((
  {onHand, open: o, completed}, 
  {type, date, quantity, id, open = [...o, ... (type == 'sale' ? [{quantity, date, id}] : [])]}
) =>  open .reduce (
  ({onHand, open, completed}, {quantity, date: saleDate, id}) => quantity <= onHand
    ? {onHand: onHand - quantity, open, completed: completed .concat ({date, id, quantity})}
    : {onHand, open: open .concat ({quantity, date: saleDate, id}), completed},
  {onHand: onHand + (type == 'purchase' ? quantity : 0) , open: [], completed}
), initial) 

const allocate = process ({onHand: 0, open: [], completed: []})

const salesOrders = [{id: "S1", created: "2020-01-02", quantity: 6}, {id: "S2", created: "2020-11-05", quantity: 2}, {id: "S3", created: "2019-12-04", quantity: 3}, {id: "S4", created: "2020-01-20", quantity: 2}, {id: "S5", created: "2019-12-15", quantity: 9}]
const purchaseOrders = [{id: "P1", receiving: "2020-01-04", quantity: 4}, {id: "P2", receiving: "2020-01-05", quantity: 3}, {id: "P3", receiving: "2020-02-01", quantity: 5}, {id: "P4", receiving: "2020-03-05", quantity: 1}, {id: "P5", receiving: "2020-02-20", quantity: 7}]

console .log (allocate (salesOrders, purchaseOrders))
.as-console-wrapper {max-height: 100% !important; top: 0}

This structure we folded to serves as our overall output, something like this:

{
  onHand: 0,
  completed: [
    {date: "2020-01-04", id: "S3", quantity: 3},
    {date: "2020-01-20", id: "S4", quantity: 2},
    {date: "2020-02-01", id: "S1", quantity: 6},
    {date: "2020-03-05", id: "S5", quantity: 9}
  ],
  open: [
    {date: "2020-11-05", id: "S2", quantity: 2}
  ]
}

But that suggests an improvement. We could make this function reentrant. We could save the output, then next time we need to add events, we could simply pass that back through the function with new sales and purchases. This would give you an updated list. It seems like a handy feature, and it's not any more difficult to add. The only real API change is that you now pass the current value in every call, with some default values for the initial call. Here is one version:

const process = (initial, sales, purchases) => [
  ...sales.map (({created, ...rest}) => ({type: 'sale', date: created, ...rest})),
  ...purchases.map (({receiving, ...rest}) => ({type: 'purchase', date: receiving, ...rest})),
] .sort (({date: d1}, {date: d2}) => d1 < d2 ? -1 : d1 > d2 ? 1 : 0) .reduce ((
  {onHand, open: o, completed}, 
  {type, date, quantity, id, open = [...o, ... (type == 'sale' ? [{quantity, date, id}] : [])]}
) =>  open .reduce (
  ({onHand, open, completed}, {quantity, date: saleDate, id}) => quantity <= onHand
    ? {onHand: onHand - quantity, open, completed: completed .concat ({date, id, quantity})}
    : {onHand, open: open .concat ({date: saleDate, id, quantity}), completed},
  {onHand: onHand + (type == 'purchase' ? quantity : 0) , open: [], completed}
), initial) 

const salesOrders = [{id: "S1", created: "2020-01-02", quantity: 6}, {id: "S2", created: "2020-11-05", quantity: 2}, {id: "S3", created: "2019-12-04", quantity: 3}, {id: "S4", created: "2020-01-20", quantity: 2}, {id: "S5", created: "2019-12-15", quantity: 9}]
const purchaseOrders = [{id: "P1", receiving: "2020-01-04", quantity: 4}, {id: "P2", receiving: "2020-01-05", quantity: 3}, {id: "P3", receiving: "2020-02-01", quantity: 5}, {id: "P4", receiving: "2020-03-05", quantity: 1}, {id: "P5", receiving: "2020-02-20", quantity: 7}]
const initialValues = {onHand: 0, open: [], completed: []}

const currentState = process (initialValues, salesOrders, purchaseOrders)
console .log ('initial load: ', currentState)

const additionalSalesOrders = [{id: "S6", created: "2021-03-07", quantity: 3},  {id: "S7", created: "2021-04-21", quantity: 10}, {id: "S3", created: "2021-06-14", quantity: 5}]
const additionalPurchaseOrders = [{id: "P6", receiving: "2021-05-20", quantity: 8}]

const nextState = process (currentState, additionalSalesOrders, additionalPurchaseOrders)
console .log ('after new events: ', nextState)
.as-console-wrapper {max-height: 100% !important; top: 0}

Upvotes: 0

user3252327
user3252327

Reputation: 627

I think your approach is mostly ok. I would just have some remarks/questions:

  • is it ok to always have stock start at 0? They are never left over? According to me, it should be one of the parameters of the allocate function
  • you should handle the case where the purchases are not enough to satisfy the sales (which is the case in your example data set) => with your current code your while loop could go over the max allowed index of the purchases array and throw an exception
  • if a sale can be satisfied before its creation date, should the availability date be before the creation date or should it be forced to the creation date? (if sales are always in the past and purchases in the future, this question doesn't make sense)

Here is how I would tackle the problem:

console.log(allocate(salesOrders, purchaseOrders));

function allocate(_sales, _purchases) {
  const sales = structureData(_sales, "created");
  const purchases = structureData(_purchases, "receiving");
  const LAST_PURCHASE_INDEX = purchases.length - 1;

  let stock = 0; // in real life, maybe this should be an input as well since it might not always start from 0?
  let availabilityDate = sales[0].date; // timestamp of stock availability, initialized to first sale timestamp
  let availabilityDateString = sales[0].created; // date in string format of stock availability, initialized to first sale created date 

  let purchaseIndex = 0; // index of the next purchase to process

  const result = [];

  // loop on sales
  for (let sale of sales) {
    const requiredQuantity = sale.quantity;
    const saleId = sale.id;

    // As long as we don't have enough stock, process the next purchase if there is any
    while (stock < requiredQuantity && purchaseIndex <= LAST_PURCHASE_INDEX) {
      const purchase = purchases[purchaseIndex];
      stock += purchase.quantity;
      availabilityDate = purchase.date;
      availabilityDateString = purchase.receiving;
      purchaseIndex++;
    }

    if (stock >= requiredQuantity) { // we have enough stock and push the availability date
      result.push({
        id: saleId,
        availabilityDate:
          availabilityDate > sale.date ? availabilityDateString : sale.created, // It could be simplified to availabilityDate if it's ok to have an availabilityDate before the sales creation
      });
      stock -= sale.quantity;
    } else { // we don't have enough stock and there are no purchases left, so we need more purchases
      result.push({ id: saleId , availabilityDateString: "Not anytime soon, need more purchases"});
    }
  }

  return result;
}

// utils to sort orders and add a date timesteamp for easier date comparison
function structureData(orders, dateField) {
  return orders
    .map((order) => ({ ...order, date: new Date(order[dateField]).getTime() }))
    .sort((o1, o2) => o1.date - o2.date);
}

Upvotes: 1

Related Questions