diff --git a/robbery.js b/robbery.js index ed84b5d..7293189 100755 --- a/robbery.js +++ b/robbery.js @@ -4,7 +4,161 @@ * Сделано задание на звездочку * Реализовано оба метода и tryLater */ -const isStar = true; +const isStar = false; + +const WEEK = ['ПН', 'ВТ', 'СР', 'ЧТ', 'ПТ', 'СБ', 'ВС']; + +class WeekDay { + constructor(weekDay) { + this.day = weekDay; + this.number = WEEK.indexOf(this.day); + } + + addDays(daysCount) { + this.number += daysCount; + this.number %= 7; + this.day = WEEK[this.number]; + } +} + +class TimeStamp { + constructor(weekDay, hours, minutes, timeZone) { + this.weekDay = weekDay ? new WeekDay(weekDay) : weekDay; + this.hours = hours; + this.minutes = minutes; + this.timeZone = timeZone; + } + + shift(timeShift) { + this.timeZone += timeShift; + this.hours += timeShift; + if (this.hours >= 24 || this.hours < 0) { + this.weekDay.addDays(this.hours < 0 ? -1 : 1); + this.hours %= 24; + } + + return this; + } + + between(time1, time2) { + if (!(time1 instanceof TimeStamp && time2 instanceof TimeStamp)) { + throw new TypeError(); + } + + return TimeStamp.compare(this, time1) >= 0 && TimeStamp.compare(this, time2) <= 0; + } + + static compare(time1, time2) { + if (!(time1 instanceof TimeStamp && time2 instanceof TimeStamp)) { + throw new TypeError(); + } + let comparator = time1.weekDay.number - time2.weekDay.number; + comparator = comparator !== 0 ? comparator : time1.hours - time2.hours; + comparator = comparator !== 0 ? comparator : time1.minutes - time2.minutes; + + return comparator; + } + + static max(time1, time2) { + if (!(time1 instanceof TimeStamp && time2 instanceof TimeStamp)) { + throw new TypeError(); + } + if (TimeStamp.compare(time1, time2) >= 0) { + return time1; + } + + return time2; + } + + static min(time1, time2) { + if (!(time1 instanceof TimeStamp && time2 instanceof TimeStamp)) { + throw new TypeError(); + } + if (TimeStamp.compare(time1, time2) <= 0) { + return time1; + } + + return time2; + } + + static parse(timeStamp) { + const splitTimeStamp = timeStamp.split(/[ +:]/).reverse(); + + return new TimeStamp( + splitTimeStamp[3], + parseInt(splitTimeStamp[2]), + parseInt(splitTimeStamp[1]), + parseInt(splitTimeStamp[0]) + ); + } +} + +class TimeInterval { + constructor(from, to) { + if (!(from instanceof TimeStamp && to instanceof TimeStamp)) { + throw new TypeError(); + } + this.from = from; + this.to = to; + } + + shift(timeShift) { + this.from.shift(timeShift); + this.to.shift(timeShift); + + return this; + } + + get duration() { + return ((this.to.weekDay.number * 24 + this.to.hours) * 60 + this.to.minutes) - + ((this.from.weekDay.number * 24 + this.from.hours) * 60 + this.from.minutes); + } + + static areIntersected(time1, time2) { + if (!(time1 instanceof TimeInterval && time2 instanceof TimeInterval)) { + throw new TypeError(); + } + + return time1.from.between(time2.from, time2.to) || time1.to.between(time2.from, time2.to) || + time2.from.between(time1.from, time1.to) || time2.to.between(time1.from, time1.to); + } + + static parse(timeInterval) { + return new TimeInterval( + TimeStamp.parse(timeInterval.from), + TimeStamp.parse(timeInterval.to) + ); + } +} + +class GangSchedule { + constructor(schedule) { + for (let robber in schedule) { + if (schedule.hasOwnProperty(robber)) { + this[robber] = schedule[robber]; + } + } + } + + forEachRobber(func, ...args) { + for (let robber in this) { + if (this.hasOwnProperty(robber)) { + this[robber] = func(this[robber], ...args); + } + } + } + + static parse(schedule) { + const gangSchedule = {}; + for (const robber in schedule) { + if (schedule.hasOwnProperty(robber)) { + gangSchedule[robber] = schedule[robber].map(time => TimeInterval.parse(time)); + } + } + + return new GangSchedule(gangSchedule); + } +} /** * @param {Object} schedule – Расписание Банды @@ -15,16 +169,31 @@ const isStar = true; * @returns {Object} */ function getAppropriateMoment(schedule, duration, workingHours) { - console.info(schedule, duration, workingHours); + const bankSchedule = ['ПН', 'ВТ', 'СР'].map(day => new TimeInterval( + TimeStamp.parse(`${day} ${workingHours.from}`), + TimeStamp.parse(`${day} ${workingHours.to}`) + )); + const bankTimeZone = bankSchedule[0].from.timeZone; + + const gangSchedule = GangSchedule.parse(schedule); + gangSchedule.forEachRobber(translateScheduleToTimeZone, bankTimeZone); + gangSchedule.forEachRobber(findFreeTimeInPeriod, new TimeInterval( + new TimeStamp('ПН', 0, 0, bankTimeZone), + new TimeStamp('СР', 23, 59, bankTimeZone) + )); + + const appropriateMoments = getAppropriateMoments(mergeSchedules(gangSchedule, bankSchedule)) + .filter(moment => moment.duration >= duration); return { + moments: appropriateMoments, /** * Найдено ли время * @returns {Boolean} */ exists: function () { - return false; + return this.moments.length !== 0; }, /** @@ -34,7 +203,15 @@ function getAppropriateMoment(schedule, duration, workingHours) { * @returns {String} */ format: function (template) { - return template; + if (!this.exists()) { + return ''; + } + const start = this.moments[0].from; + + return template + .replace(/%HH/, start.hours.toString().padStart(2, '0')) + .replace(/%MM/, start.minutes.toString().padStart(2, '0')) + .replace(/%DD/, start.weekDay.day); }, /** @@ -44,10 +221,72 @@ function getAppropriateMoment(schedule, duration, workingHours) { */ tryLater: function () { return false; + // if (!this.exists()) { + // return false; + // } } }; } +function translateScheduleToTimeZone(schedule, targetTimeZone) { + const timeZone = schedule[0].from.timeZone; + if (timeZone !== targetTimeZone) { + const timeShift = targetTimeZone - timeZone; + + return schedule.map(timeInterval => timeInterval.shift(timeShift)); + } + + return schedule; +} + +function findFreeTimeInPeriod(schedule, timePeriod) { + schedule.sort((a, b) => TimeStamp.compare(a.from, b.from)); + const freeTimes = []; + freeTimes.push({ from: timePeriod.from }); + schedule.forEach(timeInterval => { + freeTimes[freeTimes.length - 1].to = timeInterval.from; + freeTimes.push({ from: timeInterval.to }); + }); + freeTimes[freeTimes.length - 1].to = timePeriod.to; + + return freeTimes.map(interval => new TimeInterval(interval.from, interval.to)); +} + +function mergeSchedules(gangSchedule, bankSchedule) { + return Object.keys(gangSchedule) + .map(robber => gangSchedule[robber]) + .concat([bankSchedule]); +} + +function getAppropriateMoments(schedule) { + let moments = schedule[0]; + for (let i = 1; i < schedule.length; i++) { + const currentMoments = []; + const currentSchedule = schedule[i]; + for (let j = 0; j < currentSchedule.length; j++) { + const timeInterval = currentSchedule[j]; + currentMoments.push(...trimByHours(moments, timeInterval)); + } + moments = currentMoments; + } + + return moments; +} + +function trimByHours(schedule, hours) { + const trimmedSchedule = []; + schedule.forEach(timeInterval => { + if (TimeInterval.areIntersected(timeInterval, hours)) { + trimmedSchedule.push(new TimeInterval( + TimeStamp.max(timeInterval.from, hours.from), + TimeStamp.min(timeInterval.to, hours.to) + )); + } + }); + + return trimmedSchedule; +} + module.exports = { getAppropriateMoment, diff --git a/robbery.spec.js b/robbery.spec.js index 298de05..91f6fe9 100755 --- a/robbery.spec.js +++ b/robbery.spec.js @@ -48,6 +48,104 @@ describe('robbery.getAppropriateMoment()', () => { ); }); + it('ВТ у всех свободен', () => { + const moment = robbery.getAppropriateMoment({ + Danny: [ + { from: 'ПН 10:00+5', to: 'ПН 17:00+5' } + ], + Rusty: [ + { from: 'ПН 10:00+5', to: 'ПН 17:00+5' } + ], + Linus: [ + { from: 'СР 10:00+5', to: 'СР 17:00+5' } + ] + }, 120, { from: '12:00+5', to: '14:00+5' }); + + assert.ok(moment.exists()); + assert.strictEqual( + moment.format('%DD %HH:%MM'), + 'ВТ 12:00' + ); + }); + + it('вычитаем время для перевода в часовой пояс банка', () => { + const moment = robbery.getAppropriateMoment({ + Danny: [ + { from: 'ПН 14:00+7', to: 'ПН 19:00+7' }, + { from: 'ВТ 15:00+7', to: 'ВТ 18:00+7' } + ], + Rusty: [ + { from: 'ПН 11:30+5', to: 'ПН 16:30+5' }, + { from: 'ВТ 13:00+5', to: 'ВТ 16:00+5' } + ], + Linus: [ + { from: 'ПН 09:00+3', to: 'ПН 14:00+3' }, + { from: 'ПН 21:00+3', to: 'ВТ 09:30+3' }, + { from: 'СР 09:30+3', to: 'СР 15:00+3' } + ] + }, + 90, + { from: '10:00+5', to: '18:00+5' }); + + assert.ok(moment.exists()); + assert.strictEqual( + moment.format('%DD %HH:%MM'), + 'ВТ 11:30' + ); + }); + + it('перевод времени изменяет день недели', () => { + const moment = robbery.getAppropriateMoment( + { + Timmy: [ + { from: 'ПН 12:00+5', to: 'ПН 17:00+5' }, + { from: 'ВТ 13:00+5', to: 'ВТ 16:00+5' } + ], + Ben: [ + { from: 'ПН 11:30+5', to: 'ПН 16:30+5' }, + { from: 'ВТ 13:00+5', to: 'ВТ 16:00+5' } + ], + Loki: [ + { from: 'ПН 09:00+3', to: 'ПН 14:00+3' }, + { from: 'ПН 23:00+3', to: 'ВТ 09:30+3' }, + { from: 'СР 09:30+3', to: 'СР 15:00+3' } + ] + }, + 90, + { from: '10:00+5', to: '18:00+5' } + ); + + assert.ok(moment.exists()); + }); + + it('адекватное написание времени', () => { + const moment = robbery.getAppropriateMoment( + { + Danny: [ + { from: 'ПН 12:00+5', to: 'ПН 17:00+5' }, + { from: 'ВТ 13:00+5', to: 'ВТ 16:00+5' } + ], + Rusty: [ + { from: 'ПН 11:30+5', to: 'ПН 16:30+5' }, + { from: 'ВТ 13:00+5', to: 'ВТ 16:00+5' } + ], + Linus: [ + { from: 'ПН 09:00+3', to: 'ПН 14:00+3' }, + { from: 'ПН 21:00+3', to: 'ВТ 09:30+3' }, + { from: 'СР 09:30+3', to: 'СР 15:00+3' } + ] + }, + 90, + { from: '09:00+5', to: '18:00+5' } + ); + + assert.ok(moment.exists()); + assert.strictEqual( + moment.format('%DD %HH:%MM'), + 'ПН 09:00' + ); + }); + if (robbery.isStar) { it('должен перемещаться на более поздний момент [*]', () => { const moment = getMomentFor(90);