2
\$\begingroup\$

Any ideas to make it better or more efficient? any edge cases that are not covered? Opening times:

const openingTimes = [
  {
    date: "2020-01-01",
    open: "08:00",
    close: "18:00",
  },
  {
    date: "2020-01-02",
    open: "08:00",
    close: "18:00",
  },
  {
    date: "2020-01-03",
    open: "08:00",
    close: "18:00",
  },
];

Booked Appointments (busy times):

const bookedAppointments = [
  {
    date: "2020-01-01",
    time: "08:00",
  },
  {
    date: "2020-01-01",
    time: "11:00",
  },
  {
    date: "2020-01-03",
    time: "09:00",
  },
  {
    date: "2020-01-03",
    time: "10:00",
  },
  {
    date: "2020-01-03",
    time: "11:00",
  },
];

Simple Solution:

const slot_duration = 60 * 60 * 1000;
const busyTimes = bookedAppointments.map((b) =>
  Date.parse(`${b.date} ${b.time}`)
);
function isBusy(t) {
  return busyTimes.some((b) => t >= b && t < b + slot_duration);
}

function getFreeSlots() {
  const slots = {
    free: [],
    busy: [],
  };

  openingTimes.forEach((o) => {
    const start = Date.parse(`${o.date} ${o.open}`);
    const end = Date.parse(`${o.date} ${o.close}`);

    for (let i = start; i <= end; i += slot_duration) {
      if (isBusy(i)) {
        slots.busy.push(new Date(i).toLocaleString());
      } else {
        slots.free.push(new Date(i).toLocaleString());
      }
    }
  });

  return slots;
}
\$\endgroup\$
1
  • \$\begingroup\$ Wow, that's a lot of parsing and re-parsing. Better to fix up the Dates once and be done with it. Write down the requirements, as there seems to be some unspoken prohibition about seeing this in a booking: time: "11:30". Explicitly sort Dates, and then exploit binary search or 2-way merge. [Hmm, bunch of recent edits to the OP.] \$\endgroup\$
    – J_H
    Commented Mar 26, 2023 at 16:31

2 Answers 2

1
\$\begingroup\$

I think a data restructure would make management a lot easier. openingTimes could use numbers instead of strings for times so parsing is easier. bookedAppointments could change syntax to have dates as keys(then timeslots as keys):

const bookedAppointments = {
  "2020-01-01": {
    "08:00": "Davey Jones", // the value could be more complex or point to user _id
    "08:30": '',
    ...
    "11:00": "Lou Poole",
    ...
  },
  "2020-01-03": {
    "08:00": '',
    "08:30": '',
    "09:00": 'Stack Over Flow',
    "09:30": '',
    "10:00": 'Michael Scott',
    "10:30": '',
    "11:00": 'Ata',
    ...
  },
  ...
}

Another option for bookdAppointments is keys of date-hour keys: '2020-01-03 09:00': 'User', which would eliminate the nested structure, but would have a similarly easy check in isBusy

If slots followed a similar pattern then isBusy would basically be checking if object had key value:

function isBusy(date, timeSlot) {
  if(!(date in bookedAppointments)) return false;
  return bookedAppointments[date][timeSlot]
}

Other edits to get this to work involve changing slotDuration percentage of hour and a way to convert number to string time.

const openingTimes = [
  {
    date: "2020-01-01",
    open: 8,
    close: 18,
  },
  {
    date: "2020-01-02",
    open: 8,
    close: 18,
  },
  {
    date: "2020-01-03",
    open: 8,
    close: 18,
  },
];

const bookedAppointments = {
  "2020-01-01":{
    "08:00": "Davey Jones", // the value could be a more complex object with user details or point to user _id
    "11:00": "Lou Poole",
  },
  "2020-01-03":{
    "08:00": '',
    "09:00": 'Stack Over Flow',
    "10:00": 'Michael Scott',
    "11:00": 'Ata',
  },
}

const slot_duration = 1; // in hours

function padZero(string){
  return ("00" + string).slice(-2);
}
function toReadableString(time) {
  if (time < 0)
    time = 0;
  var hrs = Math.floor(time),
    mins = (time - Math.floor(time))*60;
  return `${padZero(hrs)}:${padZero(mins)}`;
}
function isBusy(date, timeSlot) {
  if(!(date in bookedAppointments)) return false;
  return bookedAppointments[date][timeSlot]
}

function getFreeSlots() {
  const slots = {
    free: {},
    busy: {},
  };

  openingTimes.forEach((o) => {
    const start = o.open;
    const end = o.close;
    let t;

    for (let i = start; i <= end; i += slot_duration) {
      t = toReadableString(i);
      if (isBusy(o.date, t)) {
        if(o.date in slots.busy) {
          slots.busy[o.date].push(t);
        } else {
          slots.busy[o.date]=[t]
        }
      } else {
        if(o.date in slots.free) {
          slots.free[o.date].push(t);
        } else {
          slots.free[o.date]=[t]
        }
      }
    }
  });

  return slots;
}
console.log(getFreeSlots())

\$\endgroup\$
1
\$\begingroup\$

Reusable package

A tidy function (class, sort of) that is not polluting global space. Try a Immediately Invoked Function Expression (IIFE). It's a bit old school but is very effective. See how that works for you.

Encapsulating "appointments" into a single coherent API makes things sooooo much easier,

Appointment object

  • Offhand with properties date, time, customer, isBooked, duration, etc.

  • put isBusy in there.

  • Make a constructor taking that string-thing object and parse to a Date.

  • override toString and add other methods as needed for outputting different formats. Possibly using a string-format expression parameter means one method can do any form.

  • Don't add to array here. Lt the Appointments (plural) objet do that.

Dates are not strings

Take advantage of built in Date functionality. Do all processing, comparisons, arithmetic as Dates not strings. Strings only for formatted output, to the console for example.

Appointment collection object

  • Make a custom list/array/collection of Appointments. There will be only one. No separate "free" & "busy", a property (above) will enable filtering.

  • openingTimes.forEach function goes in here, in a constructor is the best place,

  • Make a constructor that takes that whole appointment-string-object, makes individual Date objects, and checks for overlap as each is added to the array.

  • Have Add function for individual appointment inclusion. Check for scheduling overlap here.

The openingTimes JSON can, and should, be parsed up front, per @J_H comment

Terminology

  • Appointment and slot are the same thing. Use the same term everywhere. As written implies these are two different things/concepts, why have different names/terms otherwise?

  • OfficeHours, not "openingTimes"

  • free == not busy OR busy == not free. Don't use two different properties; actually, the point is do not have two independent pieces of code that do the same thing. It's OK if you really want both "free" and "busy" - allowing code have flexible expressiveness - make sure one is written in terms of the other. That is "free" may be coded as return !busy();

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.