需求分析
在生产实践过程中,我们接到了一个这样的需求,客户接到系统做工作单后,按照要求客服人员要定时进行回访,回访提醒必须在工作时间段内进行,提醒时间和工作时间客户要求自己设定。
算法分析
接到这个需求、首先进行分析得出了这样几个要素:
- 工作日 例如 2020-05-10 星期日(休) 2020-05-009 星期六 (法定工作日)
- 工作时段: 例如 08:30:00~12:00:00 这种时间段可能一天会设置多个
- 提醒间隔: 可以为10分钟,也可以为100分钟,跨过整个天,等等。
- 初始时间:初始时间可能是工作时间段内,也可能不在工作时间段内。
- 提醒时间: 减去非工作时间后得到在工作时间内的一个时间。
完成要素分析后,在找到对象与对象之间的关系,我们做一条时间轴,如下图所示。
首先设置初始时间,通过分析可以得出初始时间有两种可能,第一种设置的初始时间在非工作时间段那么我们则以遇到的第一个时间段的开始时间作为有效的起点时间算,第二种设置的初始时间在工作时间内,有效的起点时间就是初始时间。同理,最终计算的提醒时间,也有两种可能,第一种是在工作时间内,第二种落点不在工作时间内。也就是说通过排列组合,满足四种判断便可以遍历掉所有可能性。
结合需求,截止时间不在工作时间这个条件 是不被允许的,那么我们只要保证截止时间在工作时间内就好了,分析后最终结果为:
由于减去工作时间在计算的是否颇为麻烦,所以算法做了一次变通:引入了一个倒计时计数器,时间间隔就是计数器的初始值,公式为
计数器时间= 倒计时剩余时间 - 工作时间
直到将计数器清零为止。这就是该程序的核心算法。这个算法在实践的时候还会等价转化(关注代码)
代码清单
核心算法
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;
}
}
代码下载地址
代码下载地址