4
\$\begingroup\$

I'm trying to get the remaining days of the current week, excluding weekends and past days, and I'm lazily checking for possible continuity of the week in the next month. This is not the best solution that I can think of (and even so, perhaps there are flaws), is there a simpler and clean way to achieve what I want?

function getDaysInMonth(month, year, holidays) {
    var date = new Date(year, month, 1);
    var today = new Date();
    today.setHours(0, 0, 0, 0);
    var days = [];
    var firstDay = 0;
    var remaining = [];
    var isWeekend = (today.getDay() === 6) || (today.getDay() === 0);
    while (date.getMonth() === month) {
        if ((date >= today) && (date.getDay() != 0) && (date.getDay() != 6)) {
            days.push(new Date(date));
        }
        date.setDate(date.getDate() + 1);
    }
    month += 1;
    while (date.getMonth() === month) {
        if ((date.getDay() != 0) && (date.getDay() != 6)) {
            days.push(new Date(date));
        }
        date.setDate(date.getDate() + 1);
    }
    if (holidays) {
        for (var index = 0; index < holidays.length; index++) {
            var holiday = new Date(holidays[index]);
            for (var id = 0; id < days.length; id++) {
                if (holiday.getTime() === days[id].getTime()) {
                    days.splice(id, 1);
                    break;
                }
            }
        }
    }
    if (!isWeekend) firstDay = days[0].getDay();
    for (var index = 0; index < days.length; index++) {
        if (isWeekend) {
            remaining.push(new Date(days[index]));
            if (index === 4) break;
        } else {
            if (days[index].getDay() >= firstDay) {
                remaining.push(new Date(days[index]));
            } else {
                break;
            }
        }
    }
    return remaining;
}

Update: I've expanded the code to query for possible holidays, holidays an array of dates (mm/dd/YYY).

\$\endgroup\$
4
\$\begingroup\$

First of all, never calculate dates yourself. If possible, always defer to a library that's built by people who know more about dates, who keeps rules up to date, and has more eyes on it to spot bugs than you and your code. For that, I forward you to moment.js. With that, calculating the remaining week days would be as simple as finding today's day number with day and subtracting it from 6 (Saturday's number).

const daysUntilWeekend = 6 - moment().day()

For work days (week days sans holidays) you simply need at least 3 moment instances, the first representing the start of the week, the second representing the end of the week, and the third (or more) representing the holidays. Then you can use isBetween to check if any of your holiday dates fall between the first two, and subtract how many match from 6 accordingly.

// May fail when today's saturday or sunday. Adjust this code accordingly.
const start = moment().day(1).startOf('day')
const end = moment().day(6).startOf('day')
const holidayMoments = [moment(), moment()] // Holiday dates in moments
const holidays = holidayMoments.filter(h => h.isBetween(start, end)).length
const today = moment().day()
const remainingWorkDays = 6 - (today + holidays)
\$\endgroup\$
  • \$\begingroup\$ The idea behind my function is to get all the dates that remain and store them to further use, not count the remaining days. As for the library, I'm aware of its usefulness, but I just want to try and code by myself, I accept any constructive criticism. \$\endgroup\$ – Rick Stanley Feb 4 '18 at 17:57
0
\$\begingroup\$

Few points:

  1. Declare your variables where you are going to use them. This way, it'll be easier for you to know where you can extract methods and functionality.
  2. Keep short and descriptive methods. At the beginning, it was difficult for me to know why you were doing some hacks. If you express your intentions in the methods name I can understand your though process and the reasons behind that code.
  3. You don't need to get every weekday for this and the next month and then make the maths. You can check whether a day is weekday (or holiday) while you are iterating through month days.

Given the previous point, we could make it like this:

function getDaysInMonth(month, year, holidays) {    
    var date = getStartDate(month, year);
    return getRemainingDays(date, holidays);
}

var DAYS = {
    SUNDAY: 0,
    MONDAY: 1,
    TUESDAY: 2,
    WEDNESDAY: 3,
    THURSDAY: 4,
    FRIDAY: 5,
    SATURDAY: 6
};

function isWeekday(date){
    return !isWeekend(date);
}

function isWeekend(date){
    return date.getDay() === DAYS.SATURDAY 
    || date.getDay() === DAYS.SUNDAY;
}

function getStartDate(month, year){
    var today = new Date();
    today.setHours(0, 0, 0, 0);

    var date = new Date(year, month-1, 1);

    if(date < today){
        date = today; // We don't count past days
    }

    while(isWeekend(date)){
        date = nextDay(date); // If date is weekend, we move startDate to next Monday
    }

    return date;
}

function isHoliday(day, holidays){
    return holidays.some(holiday => day.getTime() === holiday.getTime());
}

function nextDay(date){
    var next = new Date(date);
    next.setDate(date.getDate() + 1);
    return next;
}

function getRemainingDays(date, holidays){
    var days = [];
    holidays = holidays.map(holiday => new Date(holiday));

    while(isWeekday(date)){
        if(!isHoliday(date, holidays)){
            days.push(date);
        }
        date = nextDay(date);
    }

    return days;
}
\$\endgroup\$
0
\$\begingroup\$

Time not Date

The best way to deal with dates is time in milliseconds. This is how JavaScript and most other languages deal with time, and is how a date is stored in Javascript. The value is milliseconds since midnight January 1, 1970, UTC.

Once you are working in ms all date calculations are easy.

???

Your question is "Getting the remaining days of the current week" but the functions you call is getDaysInMonth yet the function does something completely different from both the question and the function name.

I will go by what I think you want and that is a list of week days left in weeks that start in this month, excluding weekends, and holidays. I have also included a way to set the week starting day. So that you can start the week on any day, the example uses Monday as the start day.

Some points

  • Generating new Date() inside the function can cause problems. You should use a global time so that if you are using time in other functions you are working with the same time value. It is possible for consecutively executed dates date1 = new Date(); date2 = new Date(); to be on a different year, month and day.
  • The time in ms is new Date().valueOf() or new Date().getTime() though to get the time it is best to use the static method Date.now() as it avoids instantiating a Date object for no reason.
  • If you convert holidays to time in ms at start of holiday's day then to check if a time is on a holiday (time >= holiday && time < holiday + day)
  • Be aware the the standard date format is DD/MM/YYYY only the US uses MM/DD/YYYY

Constants, holidays, and dates

Convert the holidays to start of holiday's day, so you dont have to convert it each time you check the value. The example uses LOCALE to convert the date string to the correct format "World" or "US".

const DAY = 1000 * 60 * 60 * 24;  // length of day in ms
const WEEK = DAY * 7;             // length of a week in ms
const LOCALE = "World";  // or US 
const holidays = ["30/11/2018", "23/4/2012", "6/2/2018", "26/1/2018"].map(date => {
    var day, month, year;
    if (LOCALE === "US") {
        [month, day, year] = date.split("/").map(num => Number(num));
    } else {
        [day, month, year] = date.split("/").map(num => Number(num));
    }
    return new Date(year, month - 1, day).valueOf();
});

With the holidays stored as start of day in ms it is then trivial to check if a time is on a holiday

const isHoliday = time => holidays.some(start => time >= start && time < start + DAY);

Week start Day and Month week starts in

The start of the week can be different for different people. To simplify the code you can set the start of the week (week day 0) to any day.

const WEEK_START = 1; // monday = 1 <-> friday = 5 saturday  = 6 sunday = 0

You then get the week day using

const weekDay = (new Date(time).getDay() + (7 - WEEK_START)) % 7;

To get the month a week starts on using the starting day

const weekStartMonth = new Date(time - ((time + DAY * (4 - WEEK_START)) % WEEK).getMonth();

Solution

Using ms it is then just a matter of stepping over each day until the conditions of the search are met.

In this case you want days of the weeks that start in the current month, a inner loop iterated the week days (day 0 to 6) only while the outer loop has the week's starting month the same as the current week's staring month.

The results are just pushed to the array as they are found.

The function

// long names are bad, but in this case to ensure no ambiguity.
function remainingWeekDayForWeeksStartingThisMonth(time) {
  var weekday = (new Date(time).getDay() + (7 - WEEK_START)) % 7;
  time -= time % DAY;
  const currentMonth = weekStartMonth(time);
  const remaining = [];
  do {
    while (weekday < 7) { 
      if (weekday < 5 && !isHoliday(time)) {
        remaining.push(new Date(time)); 
      }
      weekday += 1; 
      time += DAY; 
    }
    weekday = 0; 
  } while (currentMonth === weekStartMonth(time));  
  return remaining;
}

The function uses some constants and function that are detailed in the snippet below and explained in the answer.

// Using US dates
const DAY = 1000 * 60 * 60 * 24;  // Length of a dat
const WEEK = DAY * 7;             // Length of a Week
const WEEK_START_OFFSET = 4;      // Offset to start of week (Sunday) or use 
                                  // new Date(0).getDay(); if you cant remember
const WEEK_START = 1;             // Day to start week on
                                  // monday = 1 <-> saturday  = 6 sunday = 0
// holidays are stored as the time in ms that the holiday starts on
const holidays = ["11/30/2018", "4/23/2018", "2/6/2018", "1/26/2018"].map(date => {
  const [month, day, year] = date.split("/").map(num => Number(num));
  return new Date(year, month - 1, day).valueOf();
});

// Function takes time in ms and returns true if holiday else false
const isHoliday = time => holidays.some(start => time >= start && time < start + DAY);
// Gets the month that the week starts in using the WEEK_START day
const weekStartMonth = time => new Date(time - ((time + DAY * (WEEK_START_OFFSET - WEEK_START)) % WEEK)).getMonth();


// long name but at least not ambiguouse
// time is time in ms
function remainingWeekDayForWeeksStartingThisMonth(time) {
  // using the week starting day get the week day
  var weekday = (new Date(time).getDay() + (7 - WEEK_START)) % 7;
  // Get the start of day time
  time -= time % DAY;
  // Using the week starting day get the month of the current week
  const currentMonth = weekStartMonth(time);
  // Results array
  const remaining = [];
  do {
    while (weekday < 7) { // do all days of the week
      const holiday = isHoliday(time);
      if (weekday < 5 && !holiday) { // and is not a holiday
        remaining.push(new Date(time)); // add the day to the remaining array
      }else{
        // just for the example to show weekends and holidays
        remaining.push(holiday ? "Holiday" : "Weekend");
      }
      weekday++; // advance a day
      time += DAY; // advance time by a day
    }
    weekday = 0; // restart week
  } while (currentMonth === weekStartMonth(time));  // Do while in weeks starting this month.
  return remaining;
}

const currentTime = Date.now();
const remainingDays = remainingWeekDayForWeeksStartingThisMonth(currentTime);

//===========================================================================
// Code to display results
const log = (data) => {
  document.body.appendChild(
    Object.assign(
      document.createElement("div"), {
        textContent: typeof data === "object" ? data.toLocaleDateString() : data,
        className: typeof data,
      }
    )
  )
}
remainingDays.forEach(log);
div {
   font-family : consolas;
   font-size : 12px;
}
.object {
   font-weight : 500;  
}
.string {
   color : red
}

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.