-1

enter image description here

I built a calendar control and was adding the week numbers as a final touch, and encountered a problem with every script example I could find on SO and outside of SO (most of which one has copied from the other).

The issue is that when dates fall in partial months, the week calculation seems to mess up and either continue counting when it is the same week in a new month, or it thinks the last full week in a previous month is the same week number as the first full new week in the following month.

Following is a visual demonstration of one of the libraries (they all have their inaccuracies as they generally base their week calculation off a fixed number and build from there) :

enter image description here

You can view the codepen here as the project is rather complex, I have the Date.prototype.getWeek function at the start to play with this easier. Feel free to swap in any code from the samples found here on SO as they all end up funking out on some months.

Some of the calculations used :

When running the most current example (2017) from "Get week of year in JavaScript like in PHP", the week returned right now is 42. When you look on my calendar, the week in October right now is showing as 42 which is correct according to here https://www.epochconverter.com/weeks/2018.

Given the example, there are full weeks sharing the same week number - so I don't see how 42 can even be accurate.

Date.prototype.getWeek = function (dowOffset) {
/*getWeek() was developed by Nick Baicoianu at MeanFreePath: http://www.epoch-calendar.com */

    dowOffset = typeof(dowOffset) == 'int' ? dowOffset : 0; //default dowOffset to zero
    var newYear = new Date(this.getFullYear(),0,1);
    var day = newYear.getDay() - dowOffset; //the day of week the year begins on
    day = (day >= 0 ? day : day + 7);
    var daynum = Math.floor((this.getTime() - newYear.getTime() - 
    (this.getTimezoneOffset()-newYear.getTimezoneOffset())*60000)/86400000) + 1;
    var weeknum;
    //if the year starts before the middle of a week
    if(day < 4) {
        weeknum = Math.floor((daynum+day-1)/7) + 1;
        if(weeknum > 52) {
            nYear = new Date(this.getFullYear() + 1,0,1);
            nday = nYear.getDay() - dowOffset;
            nday = nday >= 0 ? nday : nday + 7;
            /*if the next year starts before the middle of
              the week, it is week #1 of that year*/
            weeknum = nday < 4 ? 1 : 53;
        }
    }
    else {
        weeknum = Math.floor((daynum+day-1)/7);
    }
    return weeknum;
};

Here is some code (also tried this) that is Sunday specific (see near the bottom). I am also pasting the relevant snip here :

/* For a given date, get the ISO week number
 *
 * Based on information at:
 *
 *    http://www.merlyn.demon.co.uk/weekcalc.htm#WNR
 *
 * Algorithm is to find nearest thursday, it's year
 * is the year of the week number. Then get weeks
 * between that date and the first day of that year.
 *
 * Note that dates in one year can be weeks of previous
 * or next year, overlap is up to 3 days.
 *
 * e.g. 2014/12/29 is Monday in week  1 of 2015
 *      2012/1/1   is Sunday in week 52 of 2011
 */
function getWeekNumber(d) {
    // Copy date so don't modify original
    d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
    // Set to nearest Thursday: current date + 4 - current day number
    // Make Sunday's day number 7
    d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay()||7));
    // Get first day of year
    var yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
    // Calculate full weeks to nearest Thursday
    var weekNo = Math.ceil(( ( (d - yearStart) / 86400000) + 1)/7);
    // Return array of year and week number
    return [d.getUTCFullYear(), weekNo];
}
Kraang Prime
  • 9,981
  • 10
  • 58
  • 124
  • 1
    Uh, I believe the problem you're having is that weeks start on Sunday. If the week number calculation depends on Monday being the start of the week, then the last week of June 2018 doesn't end until the 1st of July 2018, so it's still the same week. – VLAZ Oct 20 '18 at 10:02
  • 1
    Also, I'm not sure what the problem in the last scenario is supposed to be - surely if the last week of July 2018 finishes halfway and the next half is in August 2018, then the week number will still be the same. – VLAZ Oct 20 '18 at 10:03
  • 2
    When a "week" begins is locale-specific. In the U.S., weeks start on Sunday. In most of Europe, weeks start on Monday. Different locales also use different rules for the partial initial week of a year (count it or not?). If you think the solutions are broken, you're probably just assuming a different starting point, but you've also said that you're getting 42 from the last linked solution and that your calendar and some website also agree it's week 42, so...? – T.J. Crowder Oct 20 '18 at 10:10
  • Okay, so I have posted the FULL calender with notes as to all the weirdness. 54 weeks ?? what year has 54 weeks ?? what about a consecutive month going backwards one week (eg, prior month last week was 41, next months first week is 40) .... there is more than locale start day of week stuff happening here. – Kraang Prime Oct 20 '18 at 10:20
  • @T.J.Crowder - I am in North America, so my week starts on sundays and my locale on my machine is also set as such. – Kraang Prime Oct 20 '18 at 10:29
  • @vlaz - I added a full calendar and code snip to demonstrate the issue happens with any of the example calculations that are using epoch - including ones that allegedly start from "sunday" and are flagged "working" – Kraang Prime Oct 20 '18 at 10:30
  • The last example isn't "Sunday specific", it's based on ISO week numbers which start on Monday. It returns results identical to moment.js and consistent with ISO week numbering. It also wasn't copied from anywhere, it was written from first principles. – RobG Oct 20 '18 at 10:39
  • @RobG - how would you suggest it be modified to give accurate representation for week numbers in Canada, USA and Japan since we all start on Sunday. This list here appears to be accurate : https://savvytime.com/week-number/canada/2018 . I am used to having this information readily accessible in -- well... pretty much every language natively, so this is weird for me. – Kraang Prime Oct 20 '18 at 10:48
  • 2
    If you don't want ISO week numbers, don't use code that returns ISO week numbers. What is your algorithm for week numbering? You need to state what is the first week of the year and what is the first day of the week, it all flows from there. E.g. is the first week of the year based on the first Sunday? Or the first week with more than 4 days in the year? Or…? – RobG Oct 20 '18 at 10:52
  • @RobG - starting on sundays, if a sunday to friday is the last day of the prior year, then the first week of the new year is still week 52 of the prior year. just no idea how to go about this using that enormous number to base everything from. – Kraang Prime Oct 20 '18 at 10:56
  • @KraangPrime could you explain what does the `getView` function actually do? what exactly do you want to achieve with it? – c-chavez Oct 20 '18 at 11:12
  • So (in your scheme) the first week of 2018 started on Sunday 7 January? – RobG Oct 20 '18 at 11:15
  • This code is the best example as to why I switched from JQuery to React, and from javascript weird date functions to MomentJs. @KraangPrime why not use [MomentJs](https://momentjs.com) and avoid that much trouble calculating date values, formatting, etc? I understand that sometimes you want to use your own functions for some specific functionality, but your code is very hard to debug. Is it possible for you to use MomentJs? – c-chavez Oct 20 '18 at 11:28
  • @c-chavez - sorry about the debug bit. just using codepen as a sandbox before i run it through cleanup. should have seen the first couple drafts. I did manage to get it working though by fidgeting with the calculations and then accomodating for years that end up on week 53 and 54 by just setting those to 1, and it seems the flow runs nicely. I do enjoy moment.js, but in this particular case, I am trying to do this with as little outside interference as possible. working on resolving the age horror of form design. sick of too verbose, wacky sizes, and css hacks required for basic layouts. – Kraang Prime Oct 20 '18 at 11:32
  • @c-chavez - heavy focus on chrome since there are many features completed or fairly stable in the area of form control manipulation over other browsers (eg, scroll bar styles). – Kraang Prime Oct 20 '18 at 11:34
  • @KraangPrime When I used your `pen` I modified the `getView` function where you did this: `dt = new Date(y, m, parseInt(d,10));` and changed it to `dt = new Date(y, m, 1);` and then just used `wk = m.getWeek();` in the `render` function and it worked for me. I still encourage and suggest that you use MomentJs to get rid of lots of part of that code meddling so it's more readable and easier for you to use and debug. – c-chavez Oct 20 '18 at 12:35
  • @c-chavez - I do agree that the code is a bit sloppy at the moment. The reason for 'd' is it uses whatever date the user has chosen. Code is still in progress and I may still use moment.js, but I am adopting 3rd party libraries slowly, and only if they resolve a problem that is significant and only after thorough independent testing (eg, didn't include jQuery until UI was pretty much done, then used slim, and found it was a bit too light for what I needed). I will be cleaning up the code today. A lot of code is to the view, but only about 50 lines are for handling date create/modify. – Kraang Prime Oct 20 '18 at 21:08

1 Answers1

1

The algorithm is to use the week number of the following Saturday. So get the following Saturday, then use it's year for the 1st of Jan. If it's not a Sunday, go to the previous Sunday. Then get the number of weeks from there. It might sound a bit convoluted, but it's only a few lines of code. Most of the following is helpers for playing.

Hopefully the comments are sufficient, getWeekNumber returns an array of [year, weekNumber]. Tested against the Mac OS X Calendar, which seems to use the same week numbering. Please test thoroughly, particularly around daylight saving change over.

/* Get week number in year based on:
 *  - week starts on Sunday
 *  - week number and year is that of the next Saturday,
 *    or current date if it's Saturday
 *    1st week of 2011 starts on Sunday 26 December, 2010
 *    1st week of 2017 starts on Sunday 1 January, 2017
 *
 * Calculations use UTC to avoid daylight saving issues.
 *
 * @param {Date} date - date to get week number of
 * @returns {number[]} year and week number
 */
function getWeekNumber(date) {
  // Copy date as UTC to avoid DST
  var d =  new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  // Shift to the following Saturday to get the year
  d.setUTCDate(d.getUTCDate() + 6 - d.getUTCDay());
  // Get the first day of the year
  var yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  yearStart.setUTCDate(yearStart.getUTCDate() - yearStart.getUTCDay());
  // Get difference between yearStart and d in milliseconds
  // Reduce to whole weeks
  return [d.getUTCFullYear(), (Math.ceil((d - yearStart) / 6.048e8))];
}

// Helper to format dates
function fDate(d) {
  var opts = {weekday:'short',month:'short',day:'numeric',year:'numeric'};
  return d.toLocaleString(undefined, opts);
}
// Parse yyyy-mm-dd as local
function pDate(s){
  var b = (s+'').split(/\D/);
  var d = new Date(b[0],b[1]-1,b[2]);
  return d.getMonth() == b[1]-1? d : new Date(NaN);
}
// Handle button click
function doButtonClick(){
  var d = pDate(document.getElementById('inp0').value);
  var span = document.getElementById('weekNumber');
  if (isNaN(d)) {
    span.textContent = 'Invalid date';
  } else {
    let [y,w] = getWeekNumber(d);
    span.textContent = `${fDate(d)} is in week ${w} of ${y}`;
  }
}
Date:<input id="inp0" placeholder="yyyy-mm-dd">
<button type="button" onclick="doButtonClick()">Get week number</button><br>
<span id="weekNumber"></span>
RobG
  • 142,382
  • 31
  • 172
  • 209
  • Thank you. I tested with one of the anomaly's ( 2018-07-01 and 2018-06-24 ), and they showed the correct week #'s of 27 and 26 respectively. Where did you come up with `6.048e8` for the final divisor ? – Kraang Prime Jan 10 '19 at 21:06