iT邦幫忙

1

[Javascript] 日期計算

工作上遇到一個問題,想請問各位大大的如何解決/images/emoticon/emoticon02.gif

Q: 日期加總天數,要扣除假日(六&日),計算"早於" or "晚於"的日期。

後端會給加總天數(Days),還有要計算的日期(CDate)

  1. 如果加總天數為0,則日期要等於計算的日期
  2. 加總天數為整數(0,1,2...)

目前我的思維是,先不管假日的問題,
直接把計算的日期加上總天數,會得到一個日期區間

CDate = "2021-01-04";
Days = 9;
CDate + Days = "2021-01-13";
// 日期區間為 2021-01-04 ~ 2021-01-13

然後再去把這區間的日期,所有的日期都找出來
再從這些區間的日期裡,找尋禮拜六跟禮拜日
然後再把日期往後加減

但是這樣效能很差,區間很大的話,要計算很多
但是想不到有什麼其他方法可以解決
想請問各位大大,會用什麼其他邏輯思考去解這個問題

看更多先前的討論...收起先前的討論...
powerc iT邦新手 5 級 ‧ 2021-02-26 09:58:14 檢舉
換個想法,拿CDate用for迴圈(用while也行)一天一天去加,如果遇到周末就跳過
ccutmis iT邦高手 4 級 ‧ 2021-02-26 10:14:57 檢舉
叫後端直接餵給前端計算結果,結案...haha
淺水員 iT邦研究生 2 級 ‧ 2021-02-26 11:12:15 檢舉
我比較好奇還要處理假日的版本
不知道後端送來「假日的資料」是長什麼樣子?
ekekvivi iT邦新手 5 級 ‧ 2021-02-26 11:37:07 檢舉
@powerc 這個就怕效能很差,跟我的做法有點像,都要一個一個去計算~"~

@ccutmis 我喜歡這個解法哈哈哈哈

@淺水員
後端不會送來假日的資料
後端只會傳兩個參數
一個是 增加的天數
一個是日期格式 "2021-01-02"
froce iT邦大師 1 級 ‧ 2021-02-26 16:29:21 檢舉
其實這個問題真的該給後端做吧...
工作日又不是真的就每星期扣2天就好。(茶
fillano iT邦超人 1 級 ‧ 2021-02-26 17:25:27 檢舉
後端語言有如果有Calendar相關的函式庫的話,也許後端來算比較快。另外,有些後端系統會維護一個「行事曆」,這樣參照行事曆來計算會更精準。(例如之前在基金公司,系統就有一個基金行事曆,trade day就必須要依照這個來計算)當然還要看你這樣計算的用途才會知道是否需要這些資訊。

另外,我猜後端只是懶得額外做,就單純把資料庫有的欄位吐給你,叫你自己算...
2
tunin
iT邦新手 5 級 ‧ 2021-02-26 10:16:33

用moment.js吧
參考文件,你查查看 addWeekDays() 應該就是你需要的功能。
https://momentjs.com/docs/

tunin iT邦新手 5 級 ‧ 2021-02-26 10:22:11 檢舉

另外提個解法,一週工作日是五天,所以你自己補上兩天算+7就可以計算下週的時間了,所以後端要你+10天,實際算就是+14天。

ekekvivi iT邦新手 5 級 ‧ 2021-02-26 11:38:25 檢舉

因為沒有使用moment.js
所以想用最原始的方法去解決

不過您的想法好像可行!

chan15 iT邦新手 5 級 ‧ 2021-02-27 10:42:46 檢舉

記得 moment.js 不維護了,大家現在都推 day.js

3
淺水員
iT邦研究生 2 級 ‧ 2021-02-26 10:26:32

一週用五天去算
編號:把週一當作 0,週二當作 1,週五當作 4

分別把 Days「 對 5 取餘數」、「對 5 取整數除法」
然後再去計算處理

打包成函式

function addByWorkDay(startDate, nDays)
{
    let arr=startDate.split('-').map(x=>parseInt(x,10));
    let date=new Date(arr[0], arr[1]-1, arr[2]);

    //計算需增加 nWeeks 週又 modDay 天
    let modDay=nDays%5;
    let nWeeks=(nDays-modDay)/5;

    //計算要加幾天
    let wkBegin=date.getDay();
    let dayOffset=nWeeks*7+modDay+((wkBegin+6)%7+modDay>4?2:0);

    date.setDate(date.getDate()+dayOffset);
    return [date.getFullYear(), date.getMonth()+1, date.getDate()].map(x=>x<10?'0'+x:x).join('-');
}

測試

let start='2021-01-04';
for(let i=0;i<16;++i) {
    console.log(`${start} + ${i} = ${addByWorkDay(start, i)}`);
}
淺水員 iT邦研究生 2 級 ‧ 2021-02-26 12:26:33 檢舉

我也給個有假日版本的
(也有考慮補班的狀況)

function WorkDay(data)
{
    this.arr=[]; //儲存工作日,例如 ['2020-02-01', '2020-02-02', ...]
    this.idxMap={}; //儲存「日期 => arr 的 idx」,例如 {'2020-02-01':0, '2020-02-02':1}
    this.holidays=this._arrayToMap(data.holidays);
    this.workDays=this._arrayToMap(data.workDays);
    let ts=this._strToDate(data.begin).getTime();
    let endTs=this._strToDate(data.end).getTime();
    let d=new Date;
    let tmp=[];
    while(ts<=endTs) {
        d.setTime(ts);
        let str=this._dateToStr(d);
        let wd=d.getDay();
        if((0<wd && wd<6 || this.workDays[str]) && !this.holidays[str]) {
            if(tmp.length) {
                tmp.forEach(x=>{
                    this.idxMap[x]=this.arr.length;
                });
                tmp=[];
            }
            this.idxMap[str]=this.arr.length;
            this.arr.push(str);
        } else {
            tmp.push(str);
        }
        ts+=86400000;
    }
}

WorkDay.prototype._strToDate=function (str) {
    let arr=str.split('-').map(x=>parseInt(x,10));
    return new Date(arr[0], arr[1]-1, arr[2]);
}

WorkDay.prototype._dateToStr=function(date) {
    return [date.getFullYear(), date.getMonth()+1, date.getDate()].map(x=>x<10?'0'+x:x).join('-');
}

WorkDay.prototype._arrayToMap=function(arr) {
    return arr.reduce((o, x)=>{
        o[x]=1;
        return o;
    }, {});
}

WorkDay.prototype.getOffset=function(start, nDayAdd)
{
    let x=this.idxMap[start]; //第幾個工作日
    return this.arr[x+nDayAdd]; //加上幾個工作日後回傳
}

測試

//data 是後端給的假日資料,涵蓋所有要查詢的區間
const data={
    begin: '2021-02-01',
    end: '2021-02-28',
    //假日
    holidays: ['2021-02-10', '2021-02-11', '2021-02-12', '2021-02-15', '2021-02-16'],
    //補班日期
    workDays: ['2021-02-20']
}

//依據假日資料建立 WorkDay 物件,時間複雜度跟 data.begin-data.end 成正比
const workDay=new WorkDay(data);
//之後就可以重複查詢,時間複雜度為 const
console.log(workDay.getOffset('2021-02-07', 7));
console.log(workDay.getOffset('2021-02-01', 10));
1
海綿寶寶
iT邦大神 1 級 ‧ 2021-02-26 10:52:18

另一個版本供參考

function addWorkDays(startDate, days) {
    if(isNaN(days)) {
        console.log("Value provided for \"days\" was not a number");
        return
    }
    if(!(startDate instanceof Date)) {
        console.log("Value provided for \"startDate\" was not a Date object");
        return
    }
    // Get the day of the week as a number (0 = Sunday, 1 = Monday, .... 6 = Saturday)
    var dow = startDate.getDay();
    var daysToAdd = parseInt(days);
    // If the current day is Sunday add one day
    if (dow == 0)
        daysToAdd++;
    // If the start date plus the additional days falls on or after the closest Saturday calculate weekends
    if (dow + daysToAdd >= 6) {
        //Subtract days in current working week from work days
        var remainingWorkDays = daysToAdd - (5 - dow);
        //Add current working week's weekend
        daysToAdd += 2;
        if (remainingWorkDays > 5) {
            //Add two days for each working week by calculating how many weeks are included
            daysToAdd += 2 * Math.floor(remainingWorkDays / 5);
            //Exclude final weekend if remainingWorkDays resolves to an exact number of weeks
            if (remainingWorkDays % 5 == 0)
                daysToAdd -= 2;
        }
    }
    startDate.setDate(startDate.getDate() + daysToAdd);
    return startDate;
}

//And use it like so (months are zero based)
var today = new Date(2016, 10, 22);
today = addWorkDays(today, 5);

資料來源

ekekvivi iT邦新手 5 級 ‧ 2021-02-26 11:46:48 檢舉

這個會碰到一個問題
如果日期是禮拜一, 天數是一天的話
回傳的答案會是禮拜日

我測試 2021/02/22(+1)是不會
你是用那一天測的?
https://ithelp.ithome.com.tw/upload/images/20210226/20001787XCoc5XuRMy.png

3
japhenchen
iT邦大師 1 級 ‧ 2021-02-26 11:12:40
$(document).ready(function(){
    $('#calc').click(function(){
  var d1 = $('#d1').val();
  var d2 = $('#d2').val();
        $('#dif').text(workingDaysBetweenDates(d1,d2));
    });
});
function workingDaysBetweenDates(d0, d1) {
    var startDate = parseDate(d0);
    var endDate = parseDate(d1);    
    //節日陣列,自己改一下 僅列到228補假,其他自行以行事曆列出
    var holidays = ['2021-01-01','2021-02-10','2021-02-11','2021-02-12','2021-02-13','2021-02-14','2021-02-15','2021-02-16', '2018-03-01']; 
    // 結束日一定要大於開始日,否則回傳0
    if (endDate < startDate)
        return 0;

    var z = 0; // 遇到國定假日的日數
    for (i = 0; i < holidays.length; i++)
    {
        var cand = parseDate(holidays[i]);
        var candDay = cand.getDay();

      if (cand >= startDate && cand <= endDate && candDay != 0 && candDay != 6)
      {
        // 如果假日不是星期六日,就
        z++;
      }

    }
    // 計算兩個日期間的總日數
    var millisecondsPerDay = 86400 * 1000; // 一天的總milliseconds微秒數
    startDate.setHours(0,0,0,1);  // 一天的起點
    endDate.setHours(23,59,59,999);  // 終點
    var diff = endDate - startDate;  // 兩天之間的總微秒數
    var days = Math.ceil(diff / millisecondsPerDay);

    // 算出周數weeks
    var weeks = Math.floor(days / 7);
    // 總日數 - (周數*2)=實際工作天(包含國定假日)
    days = days - (weeks * 2);

    var startDay = startDate.getDay();
    var endDay = endDate.getDay();

    // 減去周末
    if (startDay - endDay > 1)         
        days = days - 2;      

    // 如果開始日是星期天,結束日不是星期六
    if (startDay == 0 && endDay != 6)
        days = days - 1  

    // 如果結束日星期六,開始日不是星期天
    if (endDay == 6 && startDay != 0)
        days = days - 1  

    // 減掉國定假日
    return days - z;
}
function parseDate(input) {
    // 轉換日期格式
  var parts = input.match(/(\d+)/g);
  // new Date(year, month [, date [, hours[, minutes[, seconds[, ms]]]]])
  return new Date(parts[0], parts[1]-1, parts[2]); // months are 0-based
}
1
ckp6250
iT邦研究生 1 級 ‧ 2021-02-26 17:40:03

出勤系統都會遇到同樣的問題,
單單計算假日不可靠,比如說突然颱風天放假,跨月份補上班或跨月份連假,
這些都不是單純扣掉假日(六&日)就能解決的。

這個要用後端資料庫來做才會【有效能】

koei5113 iT邦新手 5 級 ‧ 2021-03-02 10:14:04 檢舉

補上班跟補連假是有 API 可以串的,颱風天那個全世界都沒辦法,都只能開發後補機制去做處理

1
froce
iT邦大師 1 級 ‧ 2021-02-26 21:24:07
Date.prototype.addDays = function(days) {
  this.setDate(this.getDate() + days);
  return this;
}

Date.prototype.addWorkDays = function(days) {
  const weekday = this.getDay() == 0? 6: this.getDay() -1
  this.addDays( parseInt(days/5)*2 + days )
  if (weekday >= 4) this.addDays( 6-weekday )
  return this;
}

我要發表回答

立即登入回答