tw-shift-schedule
Version:
586 lines (500 loc) • 18 kB
JavaScript
'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;