UNPKG

tw-shift-schedule

Version:
586 lines (500 loc) 18 kB
'use strict'; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var lexer = require('./lexer'); var transformed = { two_week: 'two_week_transformed', four_week: 'four_week_transformed', eight_week: 'eight_week_transformed' }; function validate(schedule, opts) { var ret; if (!opts) opts = {}; // FIXME: 如果班表的開頭不是變形工時週期的開頭,會無法正確判斷 switch (opts.transformed) { case undefined: ret = validateNormal(schedule, opts); break; case transformed.two_week: ret = validateTwoWeekTransformed(schedule, opts); break; case transformed.four_week: ret = validateFourWeekTransformed(schedule, opts); break; case transformed.eight_week: ret = validateEightWeekTransformed(schedule, opts); break; default: throw new Error('Invalid transform: ', opts.transformed); } for (var i = 0; i < ret.length; i++) { if (ret[i].type === 'error') { ret[i].time = offset2time(schedule.start, ret[i].offset); } } return ret; } function validateNormal(schedule) { var ret = []; var tokens = lexer(schedule.body); var invalidOffset = findInvalidToken(tokens); if (invalidOffset !== -1) { ret.push({ type: 'error', msg: '工時違法', offset: invalidOffset }); } // TODO: 一個月加班不可超過 46 小時(勞資會議可提高至 56 小時) // 按照每月切開 var overworkedMinutesPerMonth = splitByMonth(schedule.start, tokens) // 將每月按照每週切開 .map(function (_ref) { var _ref2 = _slicedToArray(_ref, 2), monthStartTime = _ref2[0], monthlyTokens = _ref2[1]; return splitBy7Day(schedule.start, monthStartTime, monthlyTokens); }) // 每週超過 40 小時的就是加班時間 .map(function (tokensPerWeekPerMonth) { return tokensPerWeekPerMonth.map(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), splitStartTime = _ref4[0], tokensPerWeek = _ref4[1]; var workMinutes = totalWorkMinutes(tokensPerWeek); return workMinutes > 40 * 60 ? workMinutes - 40 * 60 : 0; }); }) // 每個月內的加班時間各自加總 .map(function (weeklyOverworkedMinutesPerMonth) { return weeklyOverworkedMinutesPerMonth.reduce(function (x, y) { return x + y; }, 0); }); ret = ret.concat(assertMonthlyOverworkedHours(schedule.start, tokens, overworkedMinutesPerMonth, 46 * 60)); // 一週內要有一個例假 var e = assertMaxWorkDayCountInPeriod(tokens, 7, 6, '每週至少要有一個例假'); if (e) { ret.push(e); } return ret; } function validateTwoWeekTransformed(schedule) { var ret = []; var tokens = lexer(schedule.body); var invalidOffset = findInvalidToken(tokens); if (invalidOffset !== -1) { ret.push({ type: 'error', msg: '工時違法', offset: invalidOffset }); } // 如果給的班表長度小於兩週,無法真正判斷是否符合雙週變形工時 if (totalTokenLength(tokens) < 60 * 24 * 14) { ret.push({ type: 'warning', msg: '班表不完整,無法正確檢驗變形工時' }); } // TODO: 一個月加班不可超過 46 小時(勞資會議可提高至 56 小時) var overworkedMinutesPerMonth = splitByMonth(schedule.start, tokens) // 將每月按照每週切開 .map(function (_ref5) { var _ref6 = _slicedToArray(_ref5, 2), monthStartTime = _ref6[0], monthlyTokens = _ref6[1]; return splitBy7Day(schedule.start, monthStartTime, monthlyTokens); }) // 每兩週組成一個變形工時週期 .map(function (tokensPerWeekPerMonth) { return groupWeeklyTokensByNWeek(schedule.start, 2, tokensPerWeekPerMonth); }) // 每個週期超過 80 小時的就是加班時間 .map(function (tokensPer2WeekPerMonth) { return tokensPer2WeekPerMonth.map(function (_ref7) { var _ref8 = _slicedToArray(_ref7, 2), splitStartTime = _ref8[0], tokensPer2Week = _ref8[1]; var workMinutes = totalWorkMinutes(tokensPer2Week); return workMinutes > 80 * 60 ? workMinutes - 80 * 60 : 0; }); }) // 每個月內的加班時間各自加總 .map(function (biWeeklyOverworkedMinutesPerMonth) { return biWeeklyOverworkedMinutesPerMonth.reduce(function (x, y) { return x + y; }, 0); }); ret = ret.concat(assertMonthlyOverworkedHours(schedule.start, tokens, overworkedMinutesPerMonth, 46 * 60)); // 不可連續工作超過六日 var e = assertContinuousWorkDay(tokens, 6, '連續工作超過六日'); if (e) { ret.push(e); } // 兩週內要有兩個例假 e = assertMaxWorkDayCountInPeriod(tokens, 14, 12, '兩週內應有兩個例假'); if (e) { ret.push(e); } return ret; } function validateFourWeekTransformed(schedule) { var ret = []; var tokens = lexer(schedule.body); var invalidOffset = findInvalidToken(tokens); if (invalidOffset !== -1) { ret.push({ type: 'error', msg: '工時違法', offset: invalidOffset }); } // 如果給的班表長度小於四週,無法真正判斷是否符合雙週變形工時 if (totalTokenLength(tokens) < 60 * 24 * 4 * 7) { ret.push({ type: 'warning', msg: '班表不完整,無法正確檢驗變形工時' }); } // TODO: 一個月加班不可超過 46 小時(勞資會議可提高至 56 小時) var overworkedMinutesPerMonth = splitByMonth(schedule.start, tokens) // 將每月按照每週切開 .map(function (_ref9) { var _ref10 = _slicedToArray(_ref9, 2), monthStartTime = _ref10[0], monthlyTokens = _ref10[1]; return splitBy7Day(schedule.start, monthStartTime, monthlyTokens); }) // 每兩週組成一個變形工時週期 .map(function (tokensPerWeekPerMonth) { return groupWeeklyTokensByNWeek(schedule.start, 4, tokensPerWeekPerMonth); }) // 每個週期超過 160 小時的就是加班時間 .map(function (tokensPer4WeekPerMonth) { return tokensPer4WeekPerMonth.map(function (_ref11) { var _ref12 = _slicedToArray(_ref11, 2), splitStartTime = _ref12[0], tokensPer4Week = _ref12[1]; var workMinutes = totalWorkMinutes(tokensPer4Week); return workMinutes > 160 * 60 ? workMinutes - 160 * 60 : 0; }); }) // 每個月內的加班時間各自加總 .map(function (quadWeeklyOverworkedMinutesPerMonth) { return quadWeeklyOverworkedMinutesPerMonth.reduce(function (x, y) { return x + y; }, 0); }); ret = ret.concat(assertMonthlyOverworkedHours(schedule.start, tokens, overworkedMinutesPerMonth, 46 * 60)); // TODO: 如果班表長度小於一個月,且加班時數小於上限,給一個警告提醒,可能會超過上限 // 四週內要有四個例假 var e = assertMaxWorkDayCountInPeriod(tokens, 7 * 4, 7 * 4 - 4, '四週內應有四個例假'); if (e) { ret.push(e); } // 兩週內要有兩個例假 e = assertMaxWorkDayCountInPeriod(tokens, 7 * 2, 7 * 2 - 2, '每兩週內應有兩個例假'); if (e) { ret.push(e); } return ret; } function validateEightWeekTransformed(schedule) { var ret = []; var tokens = lexer(schedule.body); var invalidOffset = findInvalidToken(tokens); if (invalidOffset !== -1) { ret.push({ type: 'error', msg: '工時違法', offset: invalidOffset }); } // 如果給的班表長度小於八週,無法真正判斷是否符合雙週變形工時 if (totalTokenLength(tokens) < 60 * 24 * 8 * 7) { ret.push({ type: 'warning', msg: '班表不完整,無法正確檢驗變形工時' }); } // TODO: 如果班表長度小於一個月,且加班時數小於上限,給一個警告提醒,可能會超過上限 var weeklyTokens = splitBy7Day(schedule.start, schedule.start, tokens); var periodlyTokens = groupWeeklyTokensByNWeek(schedule.start, 8, weeklyTokens); var overworkedMinutesPerPeriod = periodlyTokens.map(function (period) { var tokensPerPeriod = period[1]; var workMinutes = totalWorkMinutes(tokensPerPeriod); return workMinutes > 320 * 60 ? workMinutes - 320 * 60 : 0; }); for (var i = 0; i < periodlyTokens.length; i++) { var _periodlyTokens$i = _slicedToArray(periodlyTokens[i], 2), periodStartTime = _periodlyTokens$i[0], periodTokens = _periodlyTokens$i[1]; var m = overworkedMinutesPerPeriod[i]; // XXX: 因為八週變形工時跨月,可以任意將休息日安排在兩月的任一天中,所以無法精確知道每個月的加班時數 // 因此,這邊直接用「是否超過兩個月的加班上限」來計算 if (m > 46 * 60 * 2) { ret.push({ type: 'error', offset: periodStartTime.clone().diff(schedule.start, 'minute'), msg: '單月加班時數超過上限', value: m / 60 }); } } // 不可連續工作超過六日 var e = assertContinuousWorkDay(tokens, 6, '連續工作超過六日'); if (e) { ret.push(e); } // 八週內要有八個例假 e = assertMaxWorkDayCountInPeriod(tokens, 7 * 8, 7 * 8 - 8, '八週內應有八個例假'); if (e) { ret.push(e); } return ret; } function findInvalidToken(tokens) { var offset = 0; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = tokens[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var t = _step.value; if (t.type === 'invalid') { return offset; } offset += t.value.length; } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return -1; } function totalTokenLength(tokens) { return tokens.map(function (t) { return t.value.length; }).reduce(function (x, sum) { return x + sum; }, 0); } function groupWeeklyTokensByNWeek(scheduleStartTime, n, tokensPerWeekPerMonth) { var group = []; var grouped = []; var groupStartTime = void 0; var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = tokensPerWeekPerMonth[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var weeklyTokens = _step2.value; var _weeklyTokens = _slicedToArray(weeklyTokens, 2), weekStartTime = _weeklyTokens[0], tokens = _weeklyTokens[1]; if (!groupStartTime) groupStartTime = weekStartTime; var weekPassed = weekStartTime.diff(groupStartTime, 'week'); if (weekPassed >= n) { grouped.push([groupStartTime, group]); group = []; groupStartTime = weekStartTime; } group = group.concat(tokens); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } if (group.length > 0) { grouped.push([groupStartTime, group]); } return grouped; } function splitBy7Day(scheduleStartTime, monthStartTime, tokens) { var currentTime = monthStartTime.clone(); var splitStartTime = monthStartTime.clone(); var split = []; var splits = []; var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = tokens[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var t = _step3.value; split.push(t); currentTime.add(t.value.length, 'minute'); if (currentTime.diff(scheduleStartTime.clone(), 'minutes') % (7 * 24 * 60) === 0) { splits.push([splitStartTime.clone(), split]); split = []; splitStartTime = currentTime.clone(); } } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } if (split.length > 0) { splits.push([splitStartTime.clone(), split]); } return splits; } function splitByMonth(startTime, tokens) { var currentTime = startTime.clone(); var splitStartTime = startTime.clone(); var split = []; var splits = []; var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = tokens[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var t = _step4.value; split.push(t); currentTime.add(t.value.length, 'minute'); if (currentTime.month() !== splitStartTime.month()) { splits.push([splitStartTime.clone(), split]); split = []; splitStartTime = currentTime.clone(); } } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } if (split.length > 0) { splits.push([splitStartTime.clone(), split]); } return splits; } function totalWorkMinutes(tokens) { var workMinutes = 0; var _iteratorNormalCompletion5 = true; var _didIteratorError5 = false; var _iteratorError5 = undefined; try { for (var _iterator5 = tokens[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { var t = _step5.value; if (t.type === 'work') { // 休息時間不計 workMinutes += t.value.split('').filter(function (c) { return c === 'x'; }).length; } } } catch (err) { _didIteratorError5 = true; _iteratorError5 = err; } finally { try { if (!_iteratorNormalCompletion5 && _iterator5.return) { _iterator5.return(); } } finally { if (_didIteratorError5) { throw _iteratorError5; } } } return workMinutes; } function findTokenTypeAt(tokens, pos) { var offset = 0; var _iteratorNormalCompletion6 = true; var _didIteratorError6 = false; var _iteratorError6 = undefined; try { for (var _iterator6 = tokens[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { var t = _step6.value; if (pos - offset < t.value.length) { return t.type; } offset += t.value.length; } } catch (err) { _didIteratorError6 = true; _iteratorError6 = err; } finally { try { if (!_iteratorNormalCompletion6 && _iterator6.return) { _iterator6.return(); } } finally { if (_didIteratorError6) { throw _iteratorError6; } } } return undefined; } function assertContinuousWorkDay(tokens, max, msg) { var continueWorked = 0; // TODO: 直接把所有 token 出來,看有沒有連續六個 work 都沒有 fullrest 即可 for (var i = 0;; i += 24 * 60) { var tokenType = findTokenTypeAt(tokens, i); if (!tokenType) break; if (tokenType === 'fullRest') { continueWorked = 0; } else { continueWorked += 1; if (continueWorked > max) { return { type: 'error', msg: msg, offset: i }; } } } return undefined; } function assertMaxWorkDayCountInPeriod(tokens, period, maxWorkDayCount, msg) { var workday = 0; for (var i = 0;; i += 24 * 60) { var tokenType = findTokenTypeAt(tokens, i); if (!tokenType) { if (workday > maxWorkDayCount) { return { type: 'error', msg: msg, offset: i }; } break; } if (tokenType !== 'fullRest') { workday += 1; } if (i !== 0 && i % (24 * 60 * (period - 1)) === 0) { if (workday > maxWorkDayCount) { return { type: 'error', msg: msg, offset: i }; } workday = 0; } } } function assertMonthlyOverworkedHours(scheduleStartTime, tokens, overworkedMinutesPerMonth, limit) { var es = []; var startTimeOfMonth = splitByMonth(scheduleStartTime, tokens).map(function (_ref13) { var _ref14 = _slicedToArray(_ref13, 2), splitStartTime = _ref14[0], monthlyTokens = _ref14[1]; return splitStartTime; }); for (var i = 0; i < overworkedMinutesPerMonth.length; i++) { var m = overworkedMinutesPerMonth[i]; if (m > limit) { es.push({ type: 'error', offset: startTimeOfMonth[i].clone().diff(scheduleStartTime, 'minute'), msg: '單月加班時數超過上限', value: m / 60 }); } } return es; } function offset2time(scheduleStartTime, offset) { if (!scheduleStartTime) { return undefined; } return scheduleStartTime.clone().add(offset, 'minute'); } module.exports = validate; module.exports.transformed = transformed;