Calculate availabilites of a resource

2019-01-29 11:51发布

I have an application displaying a week of bookable time slots (of a resource).

A time slot is always 30 min long and can either start :00 or :30.

The availabilites are internally represented by the week's minutes (in an array), 0 meaning the week's first minute at midnight and 10079 the week's last minute. This means that a resource with 100% availability have 10080 numbers in an array, 0 through 10079.

E.g. the following means that this week has 70 available minutes between 09:55-11:05 on the first day of the week:

[595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665]

How would I calculate and display any possible time slots (i.e. a minimum of 30 consecutive minutes)?

Given the data set above and the current week, and with Monday as first day of the week:

[Mon Aug 08 2016 10:00:00, Mon Aug 08 2016 10:30:00]
[Mon Aug 08 2016 10:30:00, Mon Aug 08 2016 11:00:00]

I don't know if this is easy or not, but I'm currently a bit clueless how to go about doing this in javascript? Any hints are much appreciated!

1条回答
Juvenile、少年°
2楼-- · 2019-01-29 12:29

Here's a basic function which takes an interval length, n, and an array of numbers (which represent minutes of availability).

By calling getIntervals (30) (availableMinutes) we'll get all available time slots …

Note, the range procedure below is only used to create sample data. You do not need to include it in your program.

// getIntervals :: Number -> [Number] -> [{from: Number, to: Number}]
const getIntervals = n=> availability=> {
  // intervals computed by reducing availability ...
  let {slots} = availability.reduce(({slots, count, prev}, m)=> {
    // initialize state with first minute
    if (prev === undefined)
      return {slots, count, prev: m}
    // if current interval is empty, we must begin on an interval marker
    else if (count === 0 && prev % n !== 0)
      return {slots, count, prev: m}
    // if current minute is non-sequential, restart search for next interval
    else if (prev + 1 !== m)
      return {slots, count: 0, prev: m}
    // if current interval is complete, concat valid interval
    else if (count === n - 1)
      return {slots: [...slots, {from: m - n, to: prev}], count: 0, prev: m}
    // otherwise, current minute is sequential, add to current interval
    else
      return {slots, count: count + 1, prev: m}
  }, {slots: [], count: 0, prev: undefined})
  // return `slots` value from reduce computation
  return slots
}

// range :: Number -> Number -> [Number]
const range = min=> max=> {
  let loop = (res, n) => n === max ? res : loop([...res, n], n + 1)
  return loop([], min)
}

// create sample data
let availability = [
  ...range (55) (400),     // [55, 56, 57, ..., 399]
  ...range (3111) (3333),  // [3111, 3112, 3113 ,..., 3332]
  ...range (8888) (9000)   // [8888, 8889, 8890, ..., 8999]
]

// get the intervals
console.log(getIntervals (30) (availability))

Just make sure these minute markers are minute offsets of a UTC timestamp and everything will be easy. Displaying these minute markers is just a matter of adding X minutes to a UTC timestamp that is set to the beginning of a resource's week.

We'll create a little procedure here called timestampAddMinutes which takes the minute number and converts it to a Date object based on the resource's timestamp, week

Then, we create a getIntervalDates procedure which applies that to each from and to value of each interval in our array

// timestampAddMinutes :: Date -> Number -> Date
const timestampAddMinutes = t=> m=> {
  let d = new Date(t.getTime())
  d.setMinutes(t.getMinutes() + m)
  return d
}

// getIntervalDates :: [{from: Minute, to: Minute}] -> [{from: Date, to: Date}]
const getIntervalDates = intervals => {
  return intervals.map(({from,to}) => ({
    from: timestampAddMinutes (week) (from),
    to: timestampAddMinutes (week) (to)
  }))
}
  
// sample timestamp for a resource
// Monday at midnight, timezone offset 0
let week = new Date("2016-08-08 00:00:00 +0000")

// interval result from previous code section
let intervals = [
  { from: 60, to: 89 }, { from: 90, to: 119 }, { from: 120, to: 149 },
  { from: 150, to: 179 }, { from: 180, to: 209 }, { from: 210, to: 239 },
  { from: 240, to: 269 }, { from: 270, to: 299 }, { from: 300, to: 329 },
  { from: 330, to: 359 }, { from: 360, to: 389 }, { from: 3120, to: 3149 },
  { from: 3150, to: 3179 }, { from: 3180, to: 3209 }, { from: 3210, to: 3239 },
  { from: 3240, to: 3269 }, { from: 3270, to: 3299 }, { from: 3300, to: 3329 },
  { from: 8910, to: 8939 }, { from: 8940, to: 8969 }
]

// convert intervals to dates
console.log(getIntervalDates(intervals))

You'll see that from and to were each converted to Date objects. Also pay attention to how my browser (in EDT timezone) is automatically converting the UTC timestamp to be displayed in my current timezone. The output above will show them in TZ string format, but they're fully usable Date objects. That means you can call any Date method on them to get specific details like day of week, hour, or minute, etc.

And since our getIntervals function is working properly, you'll also see that each interval is 30-minutes long and starts on :00 or :30

Output

[ { from: Sun Aug 07 2016 21:00:00 GMT-0400 (EDT), to: Sun Aug 07 2016 21:29:00 GMT-0400 (EDT) },
  { from: Sun Aug 07 2016 21:30:00 GMT-0400 (EDT), to: Sun Aug 07 2016 21:59:00 GMT-0400 (EDT) },
  { from: Sun Aug 07 2016 22:00:00 GMT-0400 (EDT), to: Sun Aug 07 2016 22:29:00 GMT-0400 (EDT) },
  { from: Sun Aug 07 2016 22:30:00 GMT-0400 (EDT), to: Sun Aug 07 2016 22:59:00 GMT-0400 (EDT) },
  { from: Sun Aug 07 2016 23:00:00 GMT-0400 (EDT), to: Sun Aug 07 2016 23:29:00 GMT-0400 (EDT) },
  { from: Sun Aug 07 2016 23:30:00 GMT-0400 (EDT), to: Sun Aug 07 2016 23:59:00 GMT-0400 (EDT) },
  { from: Mon Aug 08 2016 00:00:00 GMT-0400 (EDT), to: Mon Aug 08 2016 00:29:00 GMT-0400 (EDT) },
  { from: Mon Aug 08 2016 00:30:00 GMT-0400 (EDT), to: Mon Aug 08 2016 00:59:00 GMT-0400 (EDT) },
  { from: Mon Aug 08 2016 01:00:00 GMT-0400 (EDT), to: Mon Aug 08 2016 01:29:00 GMT-0400 (EDT) },
  { from: Mon Aug 08 2016 01:30:00 GMT-0400 (EDT), to: Mon Aug 08 2016 01:59:00 GMT-0400 (EDT) },
  { from: Mon Aug 08 2016 02:00:00 GMT-0400 (EDT), to: Mon Aug 08 2016 02:29:00 GMT-0400 (EDT) },
  { from: Wed Aug 10 2016 00:00:00 GMT-0400 (EDT), to: Wed Aug 10 2016 00:29:00 GMT-0400 (EDT) },
  { from: Wed Aug 10 2016 00:30:00 GMT-0400 (EDT), to: Wed Aug 10 2016 00:59:00 GMT-0400 (EDT) },
  { from: Wed Aug 10 2016 01:00:00 GMT-0400 (EDT), to: Wed Aug 10 2016 01:29:00 GMT-0400 (EDT) },
  { from: Wed Aug 10 2016 01:30:00 GMT-0400 (EDT), to: Wed Aug 10 2016 01:59:00 GMT-0400 (EDT) },
  { from: Wed Aug 10 2016 02:00:00 GMT-0400 (EDT), to: Wed Aug 10 2016 02:29:00 GMT-0400 (EDT) },
  { from: Wed Aug 10 2016 02:30:00 GMT-0400 (EDT), to: Wed Aug 10 2016 02:59:00 GMT-0400 (EDT) },
  { from: Wed Aug 10 2016 03:00:00 GMT-0400 (EDT), to: Wed Aug 10 2016 03:29:00 GMT-0400 (EDT) },
  { from: Sun Aug 14 2016 00:30:00 GMT-0400 (EDT), to: Sun Aug 14 2016 00:59:00 GMT-0400 (EDT) },
  { from: Sun Aug 14 2016 01:00:00 GMT-0400 (EDT), to: Sun Aug 14 2016 01:29:00 GMT-0400 (EDT) } ]

The power of a single parameter

Just to show the the flexibility of getIntervals, here's what it would look like if you changed a resource's interval length to 75 minutes (instead of 30).

// using the same `availability` input data and a 75-minute interval length
let availableDates = getIntervalDates (getIntervals (75) (availability))

console.log(availableDates)

Output — each interval is 75 minutes long and starts on a starts on a 75-minute interval marker

[ { from: Sun Aug 07 2016 21:15:00 GMT-0400 (EDT), to: Sun Aug 07 2016 22:29:00 GMT-0400 (EDT) },
  { from: Sun Aug 07 2016 22:30:00 GMT-0400 (EDT), to: Sun Aug 07 2016 23:44:00 GMT-0400 (EDT) },
  { from: Sun Aug 07 2016 23:45:00 GMT-0400 (EDT), to: Mon Aug 08 2016 00:59:00 GMT-0400 (EDT) },
  { from: Mon Aug 08 2016 01:00:00 GMT-0400 (EDT), to: Mon Aug 08 2016 02:14:00 GMT-0400 (EDT) },
  { from: Wed Aug 10 2016 00:30:00 GMT-0400 (EDT), to: Wed Aug 10 2016 01:44:00 GMT-0400 (EDT) },
  { from: Wed Aug 10 2016 01:45:00 GMT-0400 (EDT), to: Wed Aug 10 2016 02:59:00 GMT-0400 (EDT) } ]

Alternative Implementation

If you have to put comments in your code, you're probably doing something wrong. So I've broken one of my own rules by heavily relying on comments to make intent clear in the above implementation.

This implementation instead uses tiny procedures which each have immediately apparent intent. Moreover, I used a switch statement which allows us to logically group some of the loop responses without having gigantic if conditions.

// getIntervals :: Number -> [Number] -> [{from: Number, to: Number}]
const getIntervals = n=> availability=> {
  let emptyCount = x=> x === 0
  let filledCount = x=> x + 1 === n
  let invalidPrev = x=> x === undefined
  let invalidMarker = x=> x % n !== 0
  let nonsequential = (x,y)=> x + 1 !== y
  let make = (from,to) => ({from,to})
  return availability.reduce(({acc, count, prev}, m)=> {
    switch (true) {
      case invalidPrev(prev):
      case emptyCount(count) && invalidMarker(prev):
      case nonsequential(prev, m):
        return {acc, count: 0, prev: m}
      case filledCount(count):
        return {acc: [...acc, make(m - n, prev)], count: 0, prev: m}
      default:
        return {acc, count: count + 1, prev: m}
    }
  }, {acc: [], count: 0, prev: undefined}).acc
}

// range :: Number -> Number -> [Number]
const range = min=> max=> {
  let loop = (res, n) => n === max ? res : loop([...res, n], n + 1)
  return loop([], min)
}

// create sample data
let availability = [
  ...range (55) (400),     // [55, 56, 57, ..., 399]
  ...range (3111) (3333),  // [3111, 3112, 3113 ,..., 3332]
  ...range (8888) (9000)   // [8888, 8889, 8890, ..., 8999]
]

// get the intervals
console.log(getIntervals (30) (availability))

Now you can see that the loop only responds in one of 3 ways.

  1. It resets the count and updates prev to the current minute
  2. It appends a new interval (using make) to acc and does a count reset
  3. Or, as adefault case, it increments count and updates prev

Furthermore, now that the procedures have been broken down into tiny pieces, you can see how some of them could be easily reused in other areas of your application

// eq :: a -> a -> Bool
const eq = x=> y=> y === x

// isZero :: Number -> Bool
const isZero = eq (0)

// isUndefined :: a -> Bool
const isUndefined = eq (undefined)

// isSequential :: Number -> Number -> Bool
const isSequential = x=> eq (x + 1)

// isDivisibleBy :: Number -> Number -> Boolean
const isDivisibleBy = x=> y=> (isZero) (y % x)

// getIntervals :: Number -> [Number] -> [{from: Number, to: Number}]
const getIntervals = n=> availability=> {
  let make = (from,to) => ({from,to})
  return availability.reduce(({acc, count, prev}, m)=> {
    switch (true) {
      case isUndefined (prev):
      case isZero (count) && ! isDivisibleBy (n) (prev):
      case ! isSequential (prev) (m):
        return {acc, count: 0, prev: m}
      case isSequential (count) (n):
        return {acc: [...acc, make(m - n, prev)], count: 0, prev: m}
      default:
        return {acc, count: count + 1, prev: m}
    }
  }, {acc: [], count: 0, prev: undefined}).acc
}

And yep, it works the same. The idea here is if you're going to do the same thing more than once, why not define a procedure and use that instead ? By no means do you have to do go this far, but you can see how generic tiny procedures do reduce the overall complexity of getIntervals.

Now any time you want to check if two numbers are sequential, you have a procedure for that — isSequential. Same with isDivisibleBy — you can easily check if y is equally divisible by x without duplicating y % x === 0 everywhere it's needed in your app.


And, believe it or not, even these little procedures I've given you here can be broken down into more parts

// comp :: (b -> c) -> (a -> b) -> (a -> c)
const comp = f=> g=> x=> f (g (x))

// comp2 :: (c -> d) -> (a -> b -> c) -> (a -> b -> d)
const comp2 = comp (comp) (comp)

// mod :: Number -> Number -> Number
const mod = x=> y=> y % x

// isDivisibleBy :: Number -> Number -> Boolean
const isDivisibleBy = comp2 (isZero) (mod)

Bottom line is: don't go crazy with it; know when enough abstraction is enough for your particular problem. Just know that there is incredible techniques out there for abstracting complexity away. Learn the techniques and know when to apply them. You'll be happy you did ^_^

查看更多
登录 后发表回答