|
"use strict"; |
|
|
|
// Configurable constants |
|
const ASSUME_DAYTIME = true; |
|
const DAY_START_HOUR = 6; // 6 am |
|
const BLOCK_LENGTH = 0.5; // In hours, 30 minutes = 0.5 hours |
|
const WORK_DAY = [8.5, 17]; // 8:30 to 5:00 |
|
const BREAK = [12, 13]; // 12:00 to 1:00 |
|
|
|
// Constants for program logic |
|
const TIME_PARSE = /^([0-9]{1,2})\:([0-9]{1,2})$/; |
|
const MERIDIAN = 12; |
|
const MINUTES_TO_HOURS = 1/60; |
|
const HOURS_TO_MINUTES = 60; |
|
|
|
function timeStringToValue (timeString) { |
|
let parseMatches = TIME_PARSE.exec(timeString); |
|
|
|
let hour = parseInt(parseMatches[1]); |
|
if (ASSUME_DAYTIME && hour < DAY_START_HOUR) { |
|
hour += MERIDIAN; |
|
} |
|
|
|
let minute = parseInt(parseMatches[2]); |
|
|
|
return hour + minute*MINUTES_TO_HOURS; |
|
} |
|
|
|
function timeValueToString (timeValue) { |
|
let hour = parseInt(timeValue); |
|
let minute = parseInt( (timeValue - hour)*HOURS_TO_MINUTES ); |
|
let minuteString = ["0"].concat(minute.toString().split("")).slice(-2).join(""); |
|
|
|
if (ASSUME_DAYTIME && hour > MERIDIAN) { |
|
hour -= MERIDIAN; |
|
} |
|
|
|
return `${hour}:${minuteString}`; |
|
} |
|
|
|
function mergeIntervals (arrayOfIntervals) { |
|
let merged = [arrayOfIntervals[0]]; |
|
|
|
for (let i = 1; i < arrayOfIntervals.length; i++) { |
|
let lastInterval = merged[merged.length-1]; |
|
let currentInterval = arrayOfIntervals[i]; |
|
|
|
if (lastInterval[1] < currentInterval[0]) { |
|
merged.push(arrayOfIntervals[i]); |
|
} else if (lastInterval[1] < currentInterval[1]) { |
|
lastInterval[1] = currentInterval[1]; |
|
merged.pop(); |
|
merged.push(lastInterval); |
|
} |
|
} |
|
|
|
return merged; |
|
} |
|
|
|
function team_availability (timeBlocks) { |
|
let availableTimes = []; |
|
|
|
// Clone timeBlocks so original is not modified |
|
let appointments = timeBlocks.slice(0); |
|
|
|
// Convert time strings to numeric values |
|
appointments.forEach(appointment => { |
|
appointment[0] = timeStringToValue(appointment[0]); |
|
appointment[1] = timeStringToValue(appointment[1]); |
|
}); |
|
|
|
// Add lunch hour break to appointments |
|
appointments.push(BREAK.slice(0)); |
|
|
|
// Sort the appointments to make finding open blocks easier |
|
appointments.sort((blockA, blockB) => blockA[0] > blockB[0] ); |
|
|
|
// Merge appointment intervals |
|
let mergedAppointments = mergeIntervals(appointments); |
|
|
|
// Loop over time blocks, starting with beginning of work day. |
|
let time = WORK_DAY[0]; |
|
let apptIdx = 0; |
|
while ( time < WORK_DAY[1] ) { |
|
let appointmentStart, appointmentEnd; |
|
|
|
// If the index of the appointments array is still valid, compare with |
|
// the next appointment. |
|
if (apptIdx < mergedAppointments.length) { |
|
appointmentStart = mergedAppointments[apptIdx][0]; |
|
appointmentEnd = mergedAppointments[apptIdx][1]; |
|
// If there are no more appointments, compare with the end of the work day. |
|
} else { |
|
appointmentStart = WORK_DAY[1]; |
|
appointmentEnd = WORK_DAY[1]; |
|
} |
|
|
|
if (time+BLOCK_LENGTH < appointmentStart) { |
|
availableTimes.push([timeValueToString(time), timeValueToString(time+BLOCK_LENGTH)]); |
|
time += BLOCK_LENGTH; |
|
} else if (time+BLOCK_LENGTH == appointmentStart) { |
|
availableTimes.push([timeValueToString(time), timeValueToString(time+BLOCK_LENGTH)]); |
|
time = appointmentEnd; |
|
apptIdx++; |
|
} else if (time+BLOCK_LENGTH > appointmentStart) { |
|
time = appointmentEnd; |
|
apptIdx++; |
|
} else { |
|
apptIdx++; |
|
} |
|
} |
|
|
|
return availableTimes; |
|
} |
|
|
|
module.exports = team_availability; |