记一次算法在前端的实际应用


2019/9/11 算法

近期遇到个关于日历方面的需求,正好用到了一些算法方面的知识,动动小手记录一下思考过程~

执行结果1

# 需求概览

  • 具体功能:需要实现一个日历的功能,提供给系统管理员设置工作日的功能,默认是每个月周一至周五是工作日,周六至周日是非工作日(节假日暂时不考虑),管理员可以在这份基础配置上做修改,比如9月份的第二个周五是调休,则将那一天设置成非工作日。
  • 设置流程:
    1. 在选择月份之后,会调接口查询当月的工作日配置信息(第三方的接口,库里只记录工作日设置项,即如果查询的月份设置过工作日,才会有记录,如果没有设置过,则那个月份查询不到数据,不会返回默认的工作日配置🤷‍)
    2. 查询到数据则根据接口数据渲染
    3. 没有查到则需要在前端生成一份默认的工作日配置用于渲染

# 难点

对于管理员没有设置过的月份,如何在前端针对不同的月份去生成初始工作日数据?

# 需求解读

  1. 生成一份配置用于渲染,工作日设置成蓝色,非工作日设置成黄色
  2. 具体一点就是星期一至星期五设置成蓝色,星期六至星期日设置成黄色
  3. 就这意味着需要将当月的每一天跟星期(星期一:1,星期日:7)联系起来,再根据当天的星期是否大于5来区分是否是工作日

所以我们可以将需求理解为生成一份列表,里面存放某一个月每一天的日期与星期的关联数据,也就是说现在问题转换成了怎样将一个月每一天的日期与星期做关联!!

# 设计思路

以下是我的思考过程:

现在,我们现有的数据就只是用户选择了哪一年哪一个月份

为了将日期与星期做关联,我们最起码得知道这个月有多少天,这个月第一天是星期几...等等一些基本数据,这些都好办

/**
 * 获取某月份的总天数
 * @param {Number} mouth 期望获取的月份(1:一月,2:二月)
 */
const getDays = mouth => {
    const date = new Date();
    // 将当前月份设置到期望获取月份的下一个月(这里只需要将传入的月份设置上即可,因为本身传入的月份就是索引值加1)
    date.setMonth(mouth);
    // 将当前的日期置为0
    date.setDate(0);
    // 再获取天数即取上个月的天数
    return date.getDate();
}

/**
 * 获取某月第一天的星期数(星期一:1,星期日:7)
 * @param {Number} year 期望获取的年份
 * @param {Number} mouth 期望获取的月份
 */
const getFirstDayWeek = (year, mouth) => new Date(year, mouth - 1).getDay() || 7;

有了这些我们依然毫无头绪,那么换个角度来思考一下最终我们期望的目标数据结构大概是啥样的

就比如我们想要的九月的日历列表,那么我们可能希望最终的列表里有 日期(day)、星期(week)、是否是工作日(isWork,这个其实可以通过星期加上个装饰器实现),类似于下面这样:

// 九月
[
    { day: 1, week: 7, isWork: false },
    { day: 2, week: 1, isWork: true },
    { day: 3, week: 2, isWork: true },
    { day: 4, week: 3, isWork: true },
    { day: 5, week: 4, isWork: true },
    { day: 6, week: 5, isWork: true },
    { day: 7, week: 6, isWork: false },
    { day: 8, week: 7, isWork: false },
    { day: 9, week: 1, isWork: true },
    { day: 10, week: 2, isWork: true },
    { day: 11, week: 3, isWork: true },
    { day: 12, week: 4, isWork: true },
    ...
]

似乎看见了曙光!!这里的 dayweek 貌似是有规律可循的!?

# 确认数据结构及算法

  • 思路一: dayweek 应该是可以找到映射关系

    先看看几个关键的值:

    • 除去当月头几天,从当月的第一个星期一开始看(对应上面九月的 { day: 2, week: 1, isWork: true } ),第一个星期一那天 dayweek 的差值(九月对应的是 2 - 1 = 1 ),暂且叫它 diff
    • 当月的完整周的(九月中从2号开始算第一个完整周),暂且叫它 weekIndex

    我们可以得到以下公式:week = day - 7 * (weekIndex - 1) - diff (应该有其他规律,幼稚的我只能看出这样一个映射关系)

    so?这也太复杂了!!而且就算是用代码落地出来了,用右脚大拇指也能想象的到可读性非常低

  • 思路二:找找 week 字段自己的规律

    观察一下 dayweek 字段

    • day 是从1开始自增的(港废瓦):1-2-3-4-5-6-7-8-9...
    • week 是从1到7循环出现的:1-2-3-4-5-6-7-1-2-3-4...

    循环出现的 -> 头跟尾相连 -> 循环链表!!!

    明朗了!我们可以造一个 week 的循环链表

    • week 循环链表的 head 指针初始化在当月的第一天的星期数(九月对应的是星期二,则 head 指针初始化在星期二)
    • 循环一个列表(从1开始,大小为当月天数),列表中 day 即为循环索引值, week 即为当前的循环链表的 head 指针指向的值,循环的末尾更新 head 指针的位置

# 实现

落地成代码:

// 由于关于星期的数据实际上是永久化的,不存在插入和删除
// 所以这里可以偷个小懒,用字典代替循环链表,
// key 代表星期数
// value 指向下一个星期数
const dict = {
    '1': '2',
    '2': '3',
    '3': '4',
    '4': '5',
    '5': '6',
    '6': '7',
    '7': '1'
}

/**
 * 获取某月日期与星期关联列表
 * @param {Number} year 查询年份
 * @param {Number} mouth 查询月份
 */
const getCalendar = (year, mouth) => {

    // 当月天数
    const _days = getDays(mouth);
    // 当月第一天的星期数
    const _firstDayWeek = getFirstDayWeek(year, mouth);
    // 日期队列
    const _temp = [];
    // 初始化当前星期号(对应 head 指针)
    let _curWeek = _firstDayWeek + '';

    for (let i = 1; i <= _days; i++) {
        _temp.push({
            day: i,                     // 天
            week: _curWeek,             // 星期数
            isWork: +_curWeek <= 5      // 是否是工作日
        });
        // 更新当前星期号(更新 head 指针)
        _curWeek = dict[_curWeek];
    }
    return _temp;
};

当然,上述思路当然也不是唯一的,肯定还有别的更好的方式~

执行结果: 执行结果1 验证1 执行结果2 验证2

所以,别再说前端学算法用不上了🤪

Last Updated: 12/2/2019, 9:23:57 AM