Based on the its results, I’d say you’re not exactly calculating "the number of the week within the month".
Proof of this are the results for 1 and 2 January 2000, which resulted in 5
. How these days can be in the fifth week of January (since, in my understanding, the "week within of the month", would be the number of the week within the same month)? For if it is "the week within the month", then it shouldn’t be 1
?
In fact, first we have to define what a week is and how to count them within a calendar.
Definition of week
At first, a week is "any period of 7 consecutive days". But when we put that together with calendars, things get a little complicated, because there are several ways to define the "number of the week", whether for a year or a month.
There is a concept that is "the week of the year" (we are not yet going to talk about the "week within the month", but afterwards we will get there): given a certain date, I want to know which week of the year it is. And that calculation depends on two variables:
- The first day of the week
- The amount of days in the first week
The standard ISO 8601, for example, defines that the first day of the week is Monday, and the minimum amount of days in the first week is 4 (to facilitate, I will write this definition in the form (Seg, 4)
). This means that the first week of 2000 is the one that begins on a Monday and has at least 4 days in the year 2000.
If we take the calendar of the end of 1999 and the beginning of 2000, we have:
Dom Seg Ter Qua Qui Sex Sab
26 27 28 29 30 31 1 <- dez/1999 e jan/2000
2 3 4 5 6 7 8 <- jan/2000
9 10 ...
The week from 27/Dec to 2/Jan starts on a Monday, but is less than 4 days in 2000. That’s why it’s not the first week of 2000, if use the ISO 8601 definition, which is (Seg, 4)
. So, according to this definition, the first week of 2000 is from January 3rd to 9th (already the week from 12/27th to 2/jan is the 52nd week of 1999). Even, it is said that the week-based year (year based on weeks) from 01/01/2000 is 1999 (the "calendar year" is 2000, but the week-based year is 1999 - although both are "years" and have the same value for most dates, they are different concepts).
But the definition of ISO 8601, although widely used, is not the only one. The first day of the week varies greatly according to cultural and religious aspects. In many places the week is considered to begin on Saturday or Sunday, for example.
One of the attempts to standardize - or at least catalog - these definitions is the CLDR (Unicode Common Locale Data Repository), an information repository related to locales (one locale represents a "geographical region and its cultural, political, regional, etc"). The definitions of the first day of the week and the minimum number of days in the first week are in the supplementalData
:
<firstDay day="mon" territories=" 001 AD AI AL AM AN AT AX AZ BA BE BG BM BN BY CH CL CM CR CY CZ DE DK EC EE ES FI FJ FO FR GB GE GF GP GR HR HU IE IS IT KG KZ LB LI LK LT LU LV MC MD ME MK MN MQ MY NL NO NZ PL RE RO RS RU SE SI SK SM TJ TM TR UA UY UZ VA VN XK"/>
<firstDay day="fri" territories="MV"/>
<firstDay day="sat" territories="AE AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY"/>
<firstDay day="sun" territories=" AG AR AS AU BD BR BS BT BW BZ CA CN CO DM DO ET GT GU HK HN ID IL IN JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW"/>
<firstDay day="sun" territories="GB" alt="variant" references="Shorter Oxford Dictionary (5th edition, 2002)"/>
<minDays count="1" territories="001 GU UM US VI"/>
<minDays count="4" territories="AD AN AT AX BE BG CH CZ DE DK EE ES FI FJ FO FR GB GF GG GI GP GR HU IE IM IS IT JE LI LT LU MC MQ NL NO PL PT RE RU SE SJ SK SM VA" />
As we can see, the first day of the week can be Saturday, Sunday, Monday or even Friday, and some locales consider 1
as the minimum of days in the first week (other than ISO 8601, which considers 4
).
That is, if we take the same dates of Dec/1999 and Jan/2000 that we saw before:
Dom Seg Ter Qua Qui Sex Sab
26 27 28 29 30 31 1 <- dez/1999 e jan/2000
2 3 4 5 6 7 8 <- jan/2000
9 10 ...
If we use the definition (Dom, 1)
(first day of the week is Sunday, minimum days in the first week is 1), then the week from 26/ten to 1/jan becomes the first week of 2000 (as it is the first that begins on a Sunday and has at least 1 day in 2000). In this case, the days between 26 and 31 December 1999 are also part of the first week of 2000 (its week-based year is 2000, although the "calendar year" is 1999).
Weeks within a month
The ISO 8601 standard nay defines no association between months and weeks. What exist are different implementations that each language and API defines, and that make some arbitrary decisions to set the value of "week within month".
In the API java.time
, for example, it is possible to obtain the number of the week within a month in two different ways:
- considering the definition of week: in this case, if we use the ISO 8601 definition (
(Seg, 4)
), the days 1 and 2 January 2000 are not in the first week of January (which is the first period of 7 days that starts in a second and has at least 4 days in January 2000? It is not the week that has the days 1 and 2), so for those days the value of the "week within the month" is zero
- whereas the first week of the month starts on day 1: in that case, January 1 and 2 are always in the first week of the month, so the value of the "week in the month" for those days is 1
I know the focus of the question is Javascript, but I only cited the API in Java because it already provides the above two options natively (see an example). And if we look at the source code, We’ll see that these are rather boring calculations to implement manually. This is a case where reinventing the wheel would only serve for learning purposes, but if it were for a system in production, it is worth using a dedicated library (if there are people who adds libraries for much more trivial things, why not add one to more complicated things, like these calculations all I’ve mentioned?).
An alternative is the Moment js., which has methods to obtain the week of the year and the week-based year, using both the rules of locale regarding the ISO 8601 rules (and for this you will need to download the version with locales):
// locale França (Seg, 4)
moment.locale('fr-FR');
let d = moment('2000-01-01');
// semana do ano e week-based year, usando as regras do locale
console.log(`${d.week()} of ${d.weekYear()}`); // 52 of 1999
// semana do ano e week-based year, usando as regras ISO 8601
console.log(`${d.isoWeek()} of ${d.isoWeekYear()}`); // 52 of 1999
// locale Brasil (Dom, 1)
moment.locale('pt-BR');
d = moment('2000-01-01');
// semana do ano e week-based year, usando as regras do locale
console.log(`${d.week()} of ${d.weekYear()}`); // 1 of 2000
// semana do ano e week-based year, usando as regras ISO 8601
console.log(`${d.isoWeek()} of ${d.isoWeekYear()}`); // 52 of 1999
<script src="https://momentjs.com/downloads/moment-with-locales.js"></script>
If you want, you can see source code to know how the calculation is done (this particular passage can help) and build on it. But unfortunately it provides no implementation for the "week within the month".
Anyway, one way to do it would be the one below. It considers the same rule as option 1 described above ("if we use the ISO 8601 definition ((Seg, 4)
), the 1st and 2nd of January 2000 are not in the first week of January, so for them the value of the "week within the month" is zero"):
// considerando os dias da semana segundo a ISO 8601: segunda = 1, terça = 2, ..., domingo = 7
function isoWeekOfMonth(dt) { // recebe um Date e retorna a semana do mês, considerando as regras da ISO 8601
function floorDiv(x, y) {
let r = x / y;
if (r < 0) {
r = -Math.floor(-r);
} else {
r = Math.floor(r);
}
if ((x ^ y) < 0 && (r * y != x)) {
r--;
}
return r;
}
// os resultados de floorMod são diferentes do operador % quando há números negativos envolvidos
// baseado em https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#floorMod-int-int-
function floorMod(x, y) {
return x - floorDiv(x, y) * y;
}
function isoDayOfWeek(dt) {
let sow = 1; // norma ISO, semana começa na segunda
let dow = dt.getDay(); // obtém o dia da semana
if (dow == 0) { dow = 7; } // ajuste para os valores da ISO 8601
return floorMod(dow - sow, 7) + 1;
}
function startOfWeekOffset(day, dow) { 1, 6
let weekStart = floorMod(day - dow, 7);
let offset = -weekStart;
if (weekStart + 1 > 4) {
// semana anterior tem o mínimo de dias no mês atual
offset = 7 - weekStart;
}
return offset;
}
function computeWeek(offset, day) {
return Math.floor((7 + offset + (day - 1)) / 7);
}
let dow = isoDayOfWeek(dt);
let dom = dt.getDate();
let offset = startOfWeekOffset(dom, dow);
return computeWeek(offset, dom);
}
// começando em 1 de dezembro de 1999
let d = new Date(1999, 11, 1); // lembrando que janeiro é mês zero, fevereiro é 1, etc
for (let i = 0; i < 10000; i++) {
console.log(d.toLocaleDateString('pt-BR'), isoWeekOfMonth(d));
d.setDate(d.getDate() + 1);
}
The above code has been adapted from java.time
, but I didn’t do very extensive tests, I checked a few months and years and corner cases, but in broad lines this is.
The second definition is easier (from 1 to 7 January is the first week, from 8 to 14 is the second week, etc):
function weekOfMonth(dt) { // recebe um Date e retorna a semana do mês, baseado no valor numérico do dia
return Math.ceil(dt.getDate() / 7);
}
// começando em 1 de dezembro de 1999
let d = new Date(1999, 11, 1); // lembrando que janeiro é mês zero, fevereiro é 1, etc
for (let i = 0; i < 10000; i++) {
console.log(d.toLocaleDateString('pt-BR'), weekOfMonth(d));
d.setDate(d.getDate() + 1);
}