今天分享一下进公司以来我做的最“恶心”的项目中用到的一个算法。


故事背景

之所以说这个项目恶心,是因为从接到需求的那一刻起我就觉得做完这个项目我可能就会被全公司研发人员的唾沫星子给淹没,而且这个项目还映射着一个从华为挖过来的一个高管在公司里的起起落落。

这个项目的全称叫“工时任务填报审批系统”。

项目的功能是员工每日进行工时填报,每天完成了哪些工作内容,每个工作内容用了多少工时,然后提交到直接部门领导那里审批,部门领导也需要填写工时,填写的工时也由其直属领导进行审批。然后每月统计出所有员工的有效工时(领导审批时如果认为你填报的工作内容不值得你填写的那么多工时,还可以扣你的工时。。。),公司会把统计出来的工时数据作为员工绩效考核的依据。

这个项目距离现在已经时隔将近两年了,是由16年年初从华为跳槽过来的一个高管主导,然后我和公司的另外一个同事做出来的。

这位高管的职责是管理公司整个研发部门,不做技术,而是做开发流程管理,以及人员管理,其中就包括了员工的绩效考核管理。

当我接到这个需求的时候,我的心情是崩溃的,一想想经过一天的头脑风暴以后,还要花上半个小时的时间填报自己做了哪些事情,每件事情还要记住用了多少工时,填报上去的工时还可能被领导拒收。。。我就特么心累。

事实证明,不仅只有我一个人这么觉得,这系统自从上线以后,公司的研发们是怨声载道,负责审批手下员工工时填报信息的领导也觉得费时费力。苦苦支撑了不到一年的时间,从系统刚上线时的强制填写(没有及时填写还要罚款),到现在已经没有人在用了,而当初那位意气风发的高管也早已离开。。。


有效工时算法

负能量传输完毕。。。还是来聊点靠谱的,虽然这个工时填报系统做的我是浑身难受,但是在开发过程中我还是得到了不少的提升,特别是其中的有效工时算法。

这也是被吐槽的最多的一个算法,可以说是一个散发着资本主义气息的算法。 = =!

先解释一个概念,有效工时的是指填写的时间去除掉休息时间后的剩余时间。

由于员工填报的工时不是直接填写小时数目,而是选择工作时间段,包括开始时间和结束时间,系统通过这个算法会自动计算出员工选择的时间段内的有效工时,当然,考虑到有些时候员工确实会牺牲休息时间来工作,因此系统计算出的有效工时是可以自行修改的,不是只读的。

举个例子,假设公司规定中午12:00到14:00间的2小时是休息时间,如果员工填写的工作时间段是11:00到15:00,有效工时就等于(15-11)-2 = 2小时。

填报页面效果如下图:

公司规定的休息时间段除了午休时间外,往往还包括晚餐时间的一两个小时(因为往往晚上要加班- -!),这是休息的时间段还有可能调整,因此需要过滤的休息时间段是要做成可配置的。

因此这个算法难点就在于可配置的过滤时间段,以及根据过滤时间段进行有效工时的计算。

为什么说难呢?因为员工填写的时间段和过滤的时间段间的交集关系是由很多情况的,比如不相交、部分相交、全包括,不同的交集情况计算工时的方式也不同。

(如果不好理解上面这段话的话,在纸上画一画两个或多个时间段见的交集情况图会直观一些)

当过滤时间段不止一个的时候,情况会更加复杂,因此想要通过穷举出时间段的交集关系来计算有效工时是非常不靠谱的。

我最终实现的思路是,首先配置好多个过滤时间段(保证每个过滤时间段是没有交集的),然后将员工填写的时间段分别与每个过滤时间段进行比对,如果有相交则减去相交的那一段时间,这样与每个过滤时间段都进行比对和扣减之后,剩下的时间就是员工的有效工时了。

而只是比对两个时间段的相交情况就容易了,只有4种情况:用range1和range2表示两个时间段,则两个时间段的相交情况可能为:
1.range1的右侧与range2的左侧相交

2.range1的左侧与range2的右侧相交

3.range1包含range2

4.range2包含range1

代码实现如下:

//第一个休息时间范围 12:00~13:00
var timeRangeBegin1 = 12;
var timeRangeEnd1 = 13;
//第二个休息时间范围 17:30~18:30
var timeRangeBegin2 = 17.5;
var timeRangeEnd2 = 18.5;

var timeRanges = [];
timeRanges.push({
    begin: timeRangeBegin1,
    end: timeRangeEnd1
});
timeRanges.push({
    begin: timeRangeBegin2,
    end: timeRangeEnd2
});

function workHourCalculate(beginTime, endTime) {
    //时间有效性校验
    if (beginTime.getTime() >= endTime.getTime()) {
        return 0;
    }

    //开始时间
    var b = beginTime.getHours() + Number((beginTime.getMinutes() / 60).toFixed(1));
    //结束时间
    var e = endTime.getHours() + Number((endTime.getMinutes() / 60).toFixed(1));

    //总工时
    var workHour = e - b;
    //工作时间段处于休息时间段内的标志
    var includeFlag = false;
    $(timeRanges).each(function(index, timeRange) {
        var d = subTime(b, e, timeRange);
        if (d >= 0) {
            //减去与每个过滤时间段交集的时间
            workHour -= subTime(b, e, timeRange);
        } else {
            includeFlag = true;
            return false;
        }
    });
    if (includeFlag) {
        workHour = 0;
    }
    //保留1位小数
    return workHour.toFixed(1);
};

function subTime(b, e, timeRange) {
    var d = 0;
    //根据填写时间段与过滤时间段的相交情况,计算两个时间段相交的时长
    switch (true) {
        //填写时间段的右侧与过滤时间段的左侧相交,相交时长为填写结束时间 减去 过滤开始时间
        case b < timeRange.begin && e > timeRange.begin && e < timeRange.end:
            d = e - timeRange.begin;
            break;
        //填写时间段的左侧与过滤时间段的右侧相交,相交时长为过滤结束时间 减去 填写开始时间    
        case b > timeRange.begin && b < timeRange.end && e > timeRange.end:
            d = timeRange.end - b;
            break;
        //填写时间段的包含过滤时间段,相交时长为过滤时间段包含的时长
        case b <= timeRange.begin && e >= timeRange.end:
            d = timeRange.end - timeRange.begin;
            break;
        //过滤时间段包含填写时间段,相交时长为负值,
        case b >= timeRange.begin && e <= timeRange.end:
            d = -999;
            break;
    }
    //保留1位小数
    return d.toFixed(1);
}

下图是一些时间段的执行结果演示:


后话

话说我在做这个系统的时候,当时为了方便测试人员进行授权测试,还提供了一个授权页面,可以人工进行审批权限的授权,这个页面按道理正式上线以后应该禁掉的,结果我刚才去访问了一下,竟然还能访问。

在这个页面可以对任意的员工,授权任意部门的权限,这要是被别人知道了,那还不直接就玩坏了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注