Java计算下一次提醒时间的简单算法

2023-10-27

需求分析

在生产实践过程中,我们接到了一个这样的需求,客户接到系统做工作单后,按照要求客服人员要定时进行回访,回访提醒必须在工作时间段内进行,提醒时间和工作时间客户要求自己设定。

算法分析

接到这个需求、首先进行分析得出了这样几个要素:

  1. 工作日 例如 2020-05-10 星期日() 2020-05-009 星期六 (法定工作日
  2. 工作时段: 例如 08:30:00~12:00:00 这种时间段可能一天会设置多个
  3. 提醒间隔: 可以为10分钟,也可以为100分钟,跨过整个天,等等。
  4. 初始时间:初始时间可能是工作时间段内,也可能不在工作时间段内。
  5. 提醒时间: 减去非工作时间后得到在工作时间内的一个时间。

完成要素分析后,在找到对象与对象之间的关系,我们做一条时间轴,如下图所示。
时间轴
首先设置初始时间,通过分析可以得出初始时间有两种可能,第一种设置的初始时间在非工作时间段那么我们则以遇到的第一个时间段的开始时间作为有效的起点时间算,第二种设置的初始时间在工作时间内,有效的起点时间就是初始时间。同理,最终计算的提醒时间,也有两种可能,第一种是在工作时间内,第二种落点不在工作时间内。也就是说通过排列组合,满足四种判断便可以遍历掉所有可能性。
排列组合
结合需求,截止时间不在工作时间这个条件 是不被允许的,那么我们只要保证截止时间在工作时间内就好了,分析后最终结果为:

最终分析

由于减去工作时间在计算的是否颇为麻烦,所以算法做了一次变通:引入了一个倒计时计数器,时间间隔就是计数器的初始值,公式为
计数器时间= 倒计时剩余时间 - 工作时间
直到将计数器清零为止。这就是该程序的核心算法。这个算法在实践的时候还会等价转化(关注代码)

代码清单

核心算法

package com.xhd.demo;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import com.oracle.tools.packager.Log;

import java.util.Calendar;
import java.util.Date;

/**
 *
 * 机选下一次提醒时间
 * @ClassName CalculateTheNextReminderTime
 * @Description TODO
 * @Author 消魂钉
 * @Date 5/9 0009 21:58
 */
public class CalculateNextReminderTime {

    /**
     * 判断第一次情况 有两种  传入的时间不在工作区域内 和 传入的时间在工作区
     *                    下一次的时间也有两种情况 落点也有可能在工作区 和 不在工作区
     *                    这种程序递归算法为最佳选择。
     * @param date
     * @param times
     * @return
     */
    private static Timing firstStartTime(Date date,Integer times){
        //判断第一次时间是否在工作区之间

        boolean workingInterval = !WorkDayUtils.isWorkingInterval(date);
        Log.info("当前时间:"+date+":"+workingInterval);
        if (workingInterval){
            //需要转移到下一个工作区间开始工作
            //不在工作区
            Timing startTiming = WorkDayUtils.nextWorkPeriod(date,WorkDayUtils.START_DATE_TYPE);
            //第一次确认时间不在工作区的剩余时间的算法: 下一个工作区开始时间
            startTiming.setTimeLeft(times*60*1000L);
            return startTiming;
        }else{
            //在工作区
            Timing startTiming = WorkDayUtils.nextWorkPeriod(date,WorkDayUtils.END_DATE_TYPE);
            Calendar calendar = DateUtil.calendar(startTiming.getEndDate());
            long endDateTimeLong = startTiming.getEndDate().getTime();
            //第一次确认时间在工作区的算剩余时间的算法: 下次提醒的时间 - (当前时间工作区截止时间 - 当前时间) = 剩余时间
            Long timeLeft=(endDateTimeLong - date.getTime())-(times*60*1000L);
            //将剩余时间放入Timing对象中
            if (timeLeft<0){
                //如果小于0 则证明下一次提醒在本区间内。
                startTiming = WorkDayUtils.nextWorkPeriod(startTiming.getEndDate(),WorkDayUtils.START_DATE_TYPE);
                startTiming.setTimeLeft(Math.abs(timeLeft));
            }else{
                startTiming.setTimeLeft(times*60*1000L);
                startTiming.setStartDate(date);
            }
            return startTiming;
        }
    }


    /**
     * 核心递归算法:思路 以计时器的倒计时TimeLeft 为 0 作为递归的结束条件
     * @param startTime
     */
    public static Timing getNextWorkTimeDomain(Timing startTime) {
        //核心算法公式  剩余时间 = 上次剩余时间 - 工作时间时间差(时间段结束时间 - 时间段结束时间)
        Long timeDiffer = Math.abs(startTime.getTimeLeft())-(startTime.getEndDate().getTime() - startTime.getStartDate().getTime());
        if(timeDiffer>0){
            Timing nextWorkPeriod = WorkDayUtils.nextWorkPeriod(startTime);
            nextWorkPeriod.setTimeLeft(timeDiffer);
            return getNextWorkTimeDomain(nextWorkPeriod);
        }
        return startTime;
    }

    /**
     *
     * @param startDate 开始时间
     * @param times 下次提醒时间
     * @return
     */
    public static String getNextReminderTime(Date startDate,Integer times){
        return WorkDayUtils.getReminderTime(getNextWorkTimeDomain(firstStartTime(startDate,times)));
    };


    //TDD  测试驱动开发
    public static void main(String[] args) {
        //设30分钟提醒一次
        Integer times = 30;

        Integer time2 = 5*60;

        //1.第一次确认时间 在工作时间  下一次提醒时间在工作时间
        Date parse1 = DateUtil.parse("2020-05-09 09:30:00");
        //1.第一次确认时间 在工作时间  下一次提醒时间在工作时间
        Date parse1a = DateUtil.parse("2020-05-09 09:31:00");
        //2.第一次确认时间 在工作时间 下一次提醒时间不在工作时间
        Date parse2 = DateUtil.parse("2020-05-09 11:50:00");
        //3.第一次确认时间 不在工作时间 下一次提醒时间在工作时间
        Date parse3 = DateUtil.parse("2020-05-09 12:40:00");
        //4.第一次确认时间 不在工作时间 下一次提醒时间不在工作时间
        Date parse4 = DateUtil.parse("2020-05-09 12:20:00");

        //5.第二次确认时间 在工作时间  下一次提醒时间在工作时间
        Date parse5 = DateUtil.parse("2020-05-09 10:00:00");
        //6.第二次确认时间 在工作时间 下一次提醒时间不在工作时间
        Date parse6 = DateUtil.parse("2020-05-09 16:55:00");


        //7.跨天数测试 在工作时间  下一次提醒时间在工作时间
        Date parse7 = DateUtil.parse("2020-05-09 21:50:00");
        //8.跨天数测试 在工作时间 下一次提醒时间不在工作时间
        Date parse8 = DateUtil.parse("2020-05-10 15:02:00");

        System.out.println("测试结果:");


        Assert.isTrue(getNextReminderTime(parse1,times).equals("2020-05-09 10:00:00"),"");
        Assert.isTrue(getNextReminderTime(parse1a,times).equals("2020-05-09 10:01:00"),"");
        Assert.isTrue(getNextReminderTime(parse2,times).equals("2020-05-09 13:20:00"),"");
        Assert.isTrue(getNextReminderTime(parse3,times).equals("2020-05-09 13:30:00"),"");



        Assert.isTrue(getNextReminderTime(parse1,time2).equals("2020-05-09 15:30:00"),"");
        Assert.isTrue(getNextReminderTime(parse1a,time2).equals("2020-05-09 15:31:00"),"");
        Assert.isTrue(getNextReminderTime(parse2,time2).equals("2020-05-09 18:50:00"),"");
        Assert.isTrue(getNextReminderTime(parse3,time2).equals("2020-05-09 19:00:00"),"");


    }
}


计数器对象

package com.xhd.demo;

import java.util.Date;

/**
 * @ClassName TimingObj
 * @Description TODO
 * @Author 消魂钉
 * @Date 5/9 0009 23:44
 */
public class Timing {

    /**
     * 计时器开始时间
     */
    private Date startDate;
    /**
     * 计时器结束时间
     */
    private Date endDate;
    /**
     * 计时器剩余时间
     */
    private Long timeLeft;


    public Timing(Date startDate, Date endDate, Long timeLeft) {
        this.startDate = startDate;
        this.endDate = endDate;
        this.timeLeft = timeLeft;
    }

    public Timing(Date startDate, Date endDate) {
        this.startDate = startDate;
        this.endDate = endDate;
    }

    public Date getStartDate() {
        return startDate;
    }

    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }

    public Long getTimeLeft() {
        return timeLeft;
    }

    public void setTimeLeft(Long timeLeft) {
        this.timeLeft = timeLeft;
    }

    public Date getEndDate() {
        return endDate;
    }

    public void setEndDate(Date endDate) {
        this.endDate = endDate;
    }
}

工具类

package com.xhd.demo;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.*;

/**
 * @ClassName WorkDayUtils
 * @Description TODO
 * @Author 消魂钉
 * @Date 5/9 0009 21:36
 */
@Slf4j
public class WorkDayUtils {
    /**
     * 开始时间的下标
     */
    public static Integer START_DATE_TYPE = 0;
    /**
     * 结束时间的下标
     */
    public static Integer END_DATE_TYPE = 1;

    /**
     * 模拟工作时间段,可在Spring配置文件或者Mysql中读入
     * @return
     */
    public static List<String> getWorkPeriod() {
        List<String> period = new ArrayList<String>();
        period.add("08:00:00~12:00:00");
        period.add("13:00:00~17:00:00");
        period.add("18:00:00~22:00:00");
        return period;
    }

    //模拟节假日列表
    public static List<String> playday = new ArrayList<>();

    static {
        playday.add("2020-05-10");//假设是节假日
        playday.add("2020-05-12");//假设是节假日
        playday.add("2020-05-13");//假设是节假日
    }


    /**
     * 判断节假日
     *
     * @param date 传入的时间
     * @return true 表示 是节假日  false 表示不是节假日
     */
    public static boolean isPlayDay(Date date) {
        for (int i = 0; i < playday.size(); i++) {
        boolean b = compareTo(playday.get(i),DateUtil.formatDate(date));
        if (b){
            return b;
        }
    }
        return false;
}

    /**
     * 寻找非工作时段
     *
     * @param date 工作时间
     * @return true 是工作时段 false 不是工作时段
     */
    public static boolean isWorkPeriod(Date date) {
        long nowDate = date.getTime();
        String nowDateTime = DateUtil.format(date,DatePattern.NORM_DATE_FORMAT);
        List<String> workPeriod = getWorkPeriod();
        for (String s : workPeriod) {
            String[] split = s.split("~");
            log.info("isWorkPeriod方法中startTime:" + nowDateTime + " " + split[START_DATE_TYPE]);
            log.info("isWorkPeriod方法中endTime:" + nowDateTime + " " + split[END_DATE_TYPE]);
            long startTimeLong = DateUtil.parse(nowDateTime + " " + split[START_DATE_TYPE], DatePattern.NORM_DATETIME_PATTERN).getTime();
            long endTimeLong = DateUtil.parse(nowDateTime + " " + split[END_DATE_TYPE], DatePattern.NORM_DATETIME_PATTERN).getTime();
            if (startTimeLong <= nowDate && nowDate <= endTimeLong) {
                log.info("isWorkPeriod方法判断为:工作日");
                return true;
            }
        }
        log.info("isWorkPeriod方法判断为:非工作日");
        return false;
    }


    /**
     * 下一个时间在工作区内的情况     START_DATE_TYPE  END_DATE_TYPE
     *
     * @param date
     */
    public static Timing nextWorkPeriod(Date date,Integer dataType) {
        long nowDate = date.getTime();
        Map<Long, Timing> endTimeMap = getWorkPeriodList(date, getWorkPeriod(), END_DATE_TYPE);
        Set<Long> keySet = endTimeMap.keySet();
        Iterator<Long> iter = keySet.iterator();
        while (iter.hasNext()) {
            Long endTimeKey = iter.next();
            if (nowDate < endTimeKey) {
                Timing timing = endTimeMap.get(endTimeKey);
                return timing;
            }
        }
        return null;
    }


    /**
     * 获取是否在工作时间
     *
     * @param date
     * @return
     */
    public static boolean isWorkingInterval(Date date) {
        //先判断日期是否为工作日
        boolean workDay = !isPlayDay(date);
        log.info("isWorkingInterval方法的isPlayDay:"+workDay+"date:"+date);
        if (workDay) {
            //判断是否在工作时间段
            boolean workPeriod = isWorkPeriod(date);
            log.info("isWorkingInterval方法的isWorkPeriod:"+workPeriod+"date:"+date);
            if (workPeriod) {
                return true;
            }
        }
        return false;
    }

    //推算下一个工作日
    public static Date nextWorkDay(Date nextDate) {
        Calendar calendar = DateUtil.calendar(nextDate);
        calendar.add(Calendar.DAY_OF_MONTH, 1);
        nextDate = DateUtil.date(calendar);
        boolean playDay = isPlayDay(DateUtil.date(calendar));
        if (playDay) {
           return nextWorkDay(nextDate);
        }
        return nextDate;
    }



    /**
     * 最近两天的列表:注意该演示程序只寻找了当天输入和下一个工作日的所有时间区间,
     *      *              如果提醒的间隔超过24小时以上,则需要获取更多的工作日区间
     * @param nowDate
     * @param workPeriod
     * @param dateType
     * @return
     */
    private static Map<Long, Timing> getWorkPeriodList(Date nowDate, List<String> workPeriod, Integer dateType) {

        List<String> dateList = new ArrayList<>();
        //传入的时间所在天的工作日情况
        if (isPlayDay(nowDate)){
            nowDate = nextWorkDay(nowDate);
        }
        dateList.add(DateUtil.format(nowDate, DatePattern.NORM_DATE_FORMAT));
        //增强边界 找到下一个工作日
        Date nextDate = nextWorkDay(nowDate);
        log.info("找到的下一个的工作日为:"+nextDate);
        dateList.add(DateUtil.format(nextDate, DatePattern.NORM_DATE_FORMAT));
        Map<Long, Timing> timeMap = new TreeMap<>(new Comparator<Long>() {
            @Override
            public int compare(Long o1, Long o2) {
                return o1.compareTo(o2);
            }
        });
        for (String dateStr : dateList) {
            for (String s : workPeriod) {
                String[] split = s.split("~");
                //只需要保存开始的时间,最好排序
                long startTimeLong = DateUtil.parse(dateStr + " " + split[START_DATE_TYPE], DatePattern.NORM_DATETIME_PATTERN).getTime();
                long endTimeLong = DateUtil.parse(dateStr + " " + split[END_DATE_TYPE], DatePattern.NORM_DATETIME_PATTERN).getTime();
                if (dateType.compareTo(START_DATE_TYPE) == 0) {
                    timeMap.put(startTimeLong, new Timing(DateUtil.date(startTimeLong), DateUtil.date(endTimeLong)));
                } else {
                    timeMap.put(endTimeLong, new Timing(DateUtil.date(startTimeLong), DateUtil.date(endTimeLong)));
                }
            }
        }
        return timeMap;
    }

    ;

    /**
     * 两个时间比对,相同则为true 不同为false
     * @param dateNew
     * @param dateOld
     * @return
     */
    public static boolean compareTo(String dateNew, String dateOld) {
        log.info("时间比较: "+dateNew+":"+dateOld+" 结果:"+dateNew.equalsIgnoreCase(dateOld));
        if (dateNew.equalsIgnoreCase(dateOld)){
            return true;
        }else{
            return false;
        }
    }


    public static void main(String[] args) {

        boolean b = compareTo("2020-05-12", "2020-05-11");
        System.out.println(b);
    }


    /**
     * 寻找到下一个工作区
     *
     * @param time
     * @return
     */
    public static Timing nextWorkPeriod(Timing time) {
        Long startDateLong = time.getStartDate().getTime();
        Map<Long, Timing> endTimeMap = getWorkPeriodList(time.getStartDate(), getWorkPeriod(), START_DATE_TYPE);
        Set<Map.Entry<Long, Timing>> entries = endTimeMap.entrySet();
        if (CollectionUtil.isNotEmpty(entries)) {
            Iterator<Map.Entry<Long, Timing>> iterator = entries.iterator();
            while (iterator.hasNext()) {
                Map.Entry<Long, Timing> next = iterator.next();
                if (startDateLong.compareTo(next.getKey()) == 0) {
                    Timing value = iterator.next().getValue();
                    log.info("nextWorkPeriod方法:" + value.getStartDate() + ":" + value.getEndDate());
                    return value;
                }
                ;
            }
        }
        return null;
    }

    /**
     * 通过递归算法推算到最后一个工作区间的开始时间 + 最后剩余的时间,算出来的就是最终需提醒的时间。
     * @param reminderTiming
     * @return
     */
    public static String getReminderTime(Timing reminderTiming) {
        long startTime = reminderTiming.getStartDate().getTime();
        Long reminderTimeLong = startTime+reminderTiming.getTimeLeft();
        String format = DateUtil.format(DateUtil.date(reminderTimeLong), DatePattern.NORM_DATETIME_FORMAT);
        return format;
    }
}

代码下载地址

代码下载地址

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java计算下一次提醒时间的简单算法 的相关文章

随机推荐

  • JAVA字符串用两个分隔符分割的实现

    public class Test public static void main String args String s1 abc de f gAB CDE FGH String str1 s1 split for int i 0 i
  • docker 笔记

    docker 命令常用 启动容器的时候 d 后台运行 p 端口映射 宿主机 docker v 卷挂载 e 环境配置 name 容器名字 P 随机映射端口 退出docker的命令 退出docker容器而不关闭它 ctrl p q docker
  • 【翻译】图解Stable Diffusion

    原文信息 翻译自作者V2 Nov 2022版本 原文链接 The Illustrated Stable Diffusion Jay Alammar Visualizing machine learning one concept at a
  • yolov5 onnx推理 C++

    include
  • Excel中VLOOKUP函数的简易填写指南

    VLOOKUP简介 VLOOKUP函数是Excel中的一个纵向查找函数 它与LOOKUP函数和HLOOKUP函数属于一类函数 在工作中都有广泛应用 例如可以用来核对数据 多个表格之间快速导入数据等函数功能 功能是按列查找 最终返回该列所需查
  • 牛客网前端编程:合并数组 arr1 和数组 arr2。不要直接修改数组 arr,结果返回新的数组...

    方法有很多 但是思想就几种 1 两个字符直接连接起来 2 先将一个数组的字符给A 再将另一个数组的字符赋给A 本文只提供几个参考方法 方法一 使用concat function concat arr1 arr2 var arr arr ar
  • cuda 编译报错:Unresolved extern function 'cuda_tran_addr'

    出现这种问题的原因是在一个 cu文件中调用了另外一个 cu文件中的带有 device 修饰符的函数 在visual studio中需要做如下修改 如果是linux环境下需要加 dc编译选项
  • Android 10.0 系统服务之ActivityMnagerService-AMS启动流程-[Android取经之路]

    Android取经之路 的源码都基于Android Q 10 0 进行分析 Android取经之路 系列文章 系统启动篇 Android系统架构Android是怎么启动的Android 10 0系统启动之init进程Android10 0系
  • 总线周期

    3 8086执行了一个总线周期 是指8086做了哪些可能的操作 基本总线周期如何组成 在一个典型的读存储器总线周期中 地址信号 ALE信号 RD 信号 数据信号分别在何时产生 答 1 是指8086对片外的存储器或I O接口进行了一次访问 读
  • opnet 路由表

    数据结构IpT Rte Module Data是一个ip dispatch和其子进程所共有的一个数据结构 下面列出的和路由表相关的 struct IpT Rte Module Data IpT Cmn Rte Table ip route
  • 升降压电路的工作原理

    1 升压电路也叫自举电路 是利用自举升压二极管 自举升压电容等电子元件 使电容放电电压和电源电压叠加 从而使电压升高 有的电路升高的电压能达到数倍电源电压 开关直流升压电路 即所谓的boost或者step up电路 原理 the boost
  • Windows10下使用Caffe训练神经网络步骤一

    一 生成Caffe数据库 1 准备数据 参考链接中的作者提供了一些图片 共有500张图片 分为大巴车 恐龙 大象 鲜花和马五个类 每个类100张 编号分别以3 4 5 6 7开头 各为一类 其中每类选出20张用作测试 其余80张用作训练 因
  • 程序员思维

    程序员思维 起因 首先简单说一下 为什么我会想到这个话题 主要有这么几方面的原因 当我试图回过头去总结大学在计算机专业所学习的一些理论和知识的时候 发现 在学校里面学习的一些东西 走了两个极端 一个极端是偏向了细节 比如我们学习的那些 程序
  • 如何看mysql版本_如何查看mysql版本的四种方法,MySQL版本查看

    1 在终端下 mysql V 以下是代码片段 shengting login mysql V mysql Ver 14 7 Distrib 4 1 10a for redhat linux gnu i686 2 在mysql中 mysql
  • 什么是UKey?Ukey在密评中的应用 双因素身份认证 安当加密

    Ukey是什么及用途 UKey 又叫智能密码钥匙 是一种通过USB 通用串行总线接口 直接与计算机相连 具有密码验证功能 可靠高速的小型存储设备 ukey 是对现行的网络安全体系的一个极为有力的补充 基于可信计算机及智能卡技术把易用性 便携
  • ovs+dpdk 三级流表(microflow/megaflow/openflow)

    本文介绍在ovs dpdk下 三级流表的原理及其源码实现 普通模式ovs的第一和二级流表原理和ovs dpdk下的大同小异 三级流表完全一样 基本概念 microflow 最开始openflow流表是在kernel中实现的 但是因为在ker
  • uni-app发请求放在哪个生命周期函数好

    1 onLoad 页面加载了才发请求 在onLoad中发送请求是比较科学的 2 onShow 每次渲染页面就会发请求 会多次触发 会重复触发 页面隐藏显示也会触发 所以在这里发送请求不科学 3 onReady 页面初次渲染完成了 但是渲染完
  • 解决引入新的项目右边栏没有maven的问题

    解决IntejIdea引入新的项目时右侧边栏不显示maven项目的问题 导入新的idea项目的时候需要导入maven依赖 但是很多时候导入项目发现找不到maven 如图 应该怎么办呢 在help下找到find action 然后输入mave
  • <通信接口篇> I2C介绍

    目录 01 I2C总线介绍 总线物理连接 通信模式 I2C协议整体时序 02 I2C读写时序介绍 I2C写时序 主机 I2C读时序 主机 03 文章总结 大家好 这里是程序员杰克 一名平平无奇的嵌入式软件工程师 作为嵌入式开发人员 无论是硬
  • Java计算下一次提醒时间的简单算法

    Java计算下一次提醒时间的简单算法 需求分析 算法分析 代码清单 核心算法 计数器对象 工具类 代码下载地址 需求分析 在生产实践过程中 我们接到了一个这样的需求 客户接到系统做工作单后 按照要求客服人员要定时进行回访 回访提醒必须在工作