Skip to content

Commit dd32969

Browse files
authored
fix(calendar): adhere to ISO 8601 when calculating week number if the week starts on Monday (#15964)
1 parent 7167adc commit dd32969

File tree

8 files changed

+257
-32
lines changed

8 files changed

+257
-32
lines changed

projects/igniteui-angular/src/lib/calendar/calendar.component.spec.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -596,18 +596,13 @@ describe("IgxCalendar - ", () => {
596596
By.css(`${HelperTestFunctions.CALENDAR_ROW_CSSCLASS}`),
597597
);
598598

599+
const expectedWeeks = ["W", "1", "2", "3", "4", "5", "6"];
600+
599601
calendarRows.forEach((row, idx) => {
600602
const firstRowItem = row.nativeElement.children[0];
601-
602-
if (idx === 0) {
603-
expect(firstRowItem.firstChild.innerText).toEqual(
604-
"W",
605-
);
606-
} else {
607-
expect(firstRowItem.firstChild.innerText).toEqual(
608-
idx.toString(),
609-
);
610-
}
603+
expect(firstRowItem.firstChild.innerText).toEqual(
604+
expectedWeeks[idx],
605+
);
611606
});
612607
});
613608

@@ -626,12 +621,7 @@ describe("IgxCalendar - ", () => {
626621
const firstRowItem = row.nativeElement.children[0];
627622
if (idx === 5) {
628623
expect(firstRowItem.firstChild.innerText).toEqual(
629-
"13",
630-
);
631-
}
632-
if (idx === 6) {
633-
expect(firstRowItem.firstChild.innerText).toEqual(
634-
"14",
624+
"12",
635625
);
636626
}
637627
});
@@ -648,7 +638,7 @@ describe("IgxCalendar - ", () => {
648638
const firstRowItem = row.nativeElement.children[0];
649639
if (idx === 5) {
650640
expect(firstRowItem.firstChild.innerText).toEqual(
651-
"44",
641+
"43",
652642
);
653643
}
654644
});
@@ -664,6 +654,7 @@ describe("IgxCalendar - ", () => {
664654
calendarRowsDec.forEach((row, idx) => {
665655
const firstRowItem = row.nativeElement.children[0];
666656
if (idx === 6) {
657+
// With simple counting for Sunday start, expect 53
667658
expect(firstRowItem.firstChild.innerText).toEqual(
668659
"53",
669660
);

projects/igniteui-angular/src/lib/calendar/common/model.spec.ts

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ describe("Calendar Day Model", () => {
2020
const { year, month, date } = firstOfJan;
2121
expect([year, month, date]).toEqual([2024, 0, 1]);
2222

23-
// First week of 2024
23+
// First week of 2024 (ISO 8601 - January 1, 2024 is Monday, so Week 1)
2424
expect(firstOfJan.week).toEqual(1);
2525

26+
// Test week numbering with different week starts
27+
expect(firstOfJan.getWeekNumber(1)).toEqual(1); // Monday start (ISO 8601)
28+
expect(firstOfJan.getWeekNumber(0)).toBeGreaterThan(50); // Sunday start (belongs to prev year)
29+
2630
// 2024/01/01 is a Monday
2731
expect(firstOfJan.day).toEqual(1);
2832
expect(firstOfJan.weekend).toBeFalse();
@@ -79,6 +83,124 @@ describe("Calendar Day Model", () => {
7983
});
8084
});
8185

86+
describe("Week numbering", () => {
87+
it("should use ISO 8601 for Monday start and simple counting for others", () => {
88+
// January 1, 2025 is a Wednesday
89+
const jan1_2025 = new CalendarDay({ year: 2025, month: 0, date: 1 });
90+
expect(jan1_2025.day).toEqual(3); // Wednesday
91+
92+
// Monday start: Uses ISO 8601 standard
93+
expect(jan1_2025.getWeekNumber(1)).toEqual(1); // Week 1 contains Jan 1
94+
95+
// Sunday start: Uses simple counting - Jan 1 (Wed) belongs to prev year
96+
expect(jan1_2025.getWeekNumber(0)).toBeGreaterThan(50); // Week 52 of 2024
97+
});
98+
99+
it("should handle ISO 8601 year boundaries for Monday start", () => {
100+
// January 1, 2026 is a Thursday
101+
const jan1_2026 = new CalendarDay({ year: 2026, month: 0, date: 1 });
102+
expect(jan1_2026.day).toEqual(4); // Thursday
103+
104+
// Monday start: ISO 8601 logic applies
105+
expect(jan1_2026.getWeekNumber(1)).toEqual(1); // Week 1 of 2026
106+
});
107+
108+
it("should handle previous year's last week for Monday start", () => {
109+
// January 1, 2027 is a Friday
110+
const jan1_2027 = new CalendarDay({ year: 2027, month: 0, date: 1 });
111+
expect(jan1_2027.day).toEqual(5); // Friday
112+
113+
// Monday start: ISO 8601 logic - belongs to previous year
114+
const actualWeek = jan1_2027.getWeekNumber(1);
115+
expect(actualWeek).toBeGreaterThan(50); // Should be Week 52 or 53 of 2026
116+
});
117+
118+
it("should work correctly with custom week starts using appropriate logic", () => {
119+
const testDate = new CalendarDay({ year: 2024, month: 2, date: 15 }); // March 15, 2024 (Friday)
120+
121+
// Test different week start days
122+
const mondayStart = testDate.getWeekNumber(1); // ISO 8601
123+
const tuesdayStart = testDate.getWeekNumber(2); // Simple counting
124+
const wednesdayStart = testDate.getWeekNumber(3); // Simple counting
125+
const thursdayStart = testDate.getWeekNumber(4); // Simple counting
126+
const fridayStart = testDate.getWeekNumber(5); // Simple counting
127+
const saturdayStart = testDate.getWeekNumber(6); // Simple counting
128+
const sundayStart = testDate.getWeekNumber(0); // Simple counting
129+
130+
// All should be valid week numbers (positive integers)
131+
expect(mondayStart).toBeGreaterThan(0);
132+
expect(tuesdayStart).toBeGreaterThan(0);
133+
expect(wednesdayStart).toBeGreaterThan(0);
134+
expect(thursdayStart).toBeGreaterThan(0);
135+
expect(fridayStart).toBeGreaterThan(0);
136+
expect(saturdayStart).toBeGreaterThan(0);
137+
expect(sundayStart).toBeGreaterThan(0);
138+
});
139+
140+
it("should apply ISO 8601 logic only for Monday start", () => {
141+
// January 4, 2024 is a Thursday - always Week 1 in ISO 8601
142+
const jan4_2024 = new CalendarDay({ year: 2024, month: 0, date: 4 });
143+
expect(jan4_2024.day).toEqual(4); // Thursday
144+
145+
// Only Monday start uses ISO 8601
146+
expect(jan4_2024.getWeekNumber(1)).toEqual(1); // Monday start: ISO 8601
147+
148+
// Other starts use simple counting, so results may vary
149+
const sundayWeek = jan4_2024.getWeekNumber(0);
150+
const tuesdayWeek = jan4_2024.getWeekNumber(2);
151+
expect(sundayWeek).toBeGreaterThan(0);
152+
expect(tuesdayWeek).toBeGreaterThan(0);
153+
});
154+
155+
it("should handle December dates that belong to next year's Week 1 for Monday start", () => {
156+
// December 30, 2024 is a Monday
157+
const dec30_2024 = new CalendarDay({ year: 2024, month: 11, date: 30 });
158+
expect(dec30_2024.day).toEqual(1); // Monday
159+
160+
// Monday start: This should be Week 1 of 2025 in ISO 8601
161+
expect(dec30_2024.getWeekNumber(1)).toEqual(1); // Week 1 of 2025
162+
});
163+
164+
it("should default to Monday start when no parameter provided", () => {
165+
const testDate = new CalendarDay({ year: 2024, month: 0, date: 1 });
166+
167+
// Should default to Monday start (ISO 8601 standard)
168+
expect(testDate.getWeekNumber()).toEqual(testDate.getWeekNumber(1));
169+
expect(testDate.week).toEqual(testDate.getWeekNumber(1));
170+
});
171+
172+
it("should handle leap years correctly", () => {
173+
// Test February 29, 2024 (leap year)
174+
const feb29_2024 = new CalendarDay({ year: 2024, month: 1, date: 29 });
175+
expect(feb29_2024.day).toEqual(4); // Thursday
176+
177+
// Should calculate week number correctly for leap year date
178+
const weekNumber = feb29_2024.getWeekNumber(1);
179+
expect(weekNumber).toBeGreaterThan(0);
180+
expect(weekNumber).toBeLessThan(54); // Valid week range
181+
});
182+
183+
it("should correctly handle the January 2024 Sunday start case", () => {
184+
// January 1, 2024 is a Monday, with Sunday start (0)
185+
const jan1_2024 = new CalendarDay({ year: 2024, month: 0, date: 1 });
186+
const jan7_2024 = new CalendarDay({ year: 2024, month: 0, date: 7 }); // Sunday
187+
const jan8_2024 = new CalendarDay({ year: 2024, month: 0, date: 8 }); // Monday
188+
189+
expect(jan1_2024.day).toEqual(1); // Monday
190+
expect(jan7_2024.day).toEqual(0); // Sunday
191+
expect(jan8_2024.day).toEqual(1); // Monday
192+
193+
// With Sunday start, Jan 1 should be in previous year's last week
194+
expect(jan1_2024.getWeekNumber(0)).toBeGreaterThan(50); // Week 53 of 2023
195+
196+
// Jan 7 (first Sunday) should be Week 1
197+
expect(jan7_2024.getWeekNumber(0)).toEqual(1);
198+
199+
// Jan 8 should also be Week 1 (same week as Jan 7)
200+
expect(jan8_2024.getWeekNumber(0)).toEqual(1);
201+
});
202+
});
203+
82204
describe("Date ranges", () => {
83205
start = new CalendarDay({ year: 2024, month: 0, date: 11 });
84206
const endFuture = start.add("day", 7);

projects/igniteui-angular/src/lib/calendar/common/model.ts

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,104 @@ export class CalendarDay {
115115
return this._date.getTime();
116116
}
117117

118-
/** Returns the current week number. */
118+
/** Returns the ISO 8601 week number. */
119119
public get week() {
120-
const firstDay = new CalendarDay({ year: this.year, month: 0 })
121-
.timestamp;
122-
const currentDay =
123-
(this.timestamp - firstDay + millisecondsInDay) / millisecondsInDay;
124-
return Math.ceil(currentDay / daysInWeek);
120+
return this.getWeekNumber();
121+
}
122+
123+
/**
124+
* Gets the week number based on week start day.
125+
* Uses ISO 8601 (first Thursday rule) only when weekStart is Monday (1).
126+
* For other week starts, uses simple counting from January 1st.
127+
*/
128+
public getWeekNumber(weekStart: number = 1): number {
129+
if (weekStart === 1) {
130+
return this.calculateISO8601WeekNumber();
131+
} else {
132+
return this.calculateSimpleWeekNumber(weekStart);
133+
}
134+
}
135+
136+
/**
137+
* Calculates week number using ISO 8601 standard (Monday start, first Thursday rule).
138+
*/
139+
private calculateISO8601WeekNumber(): number {
140+
const currentThursday = this.getThursdayOfWeek();
141+
const firstWeekThursday = this.getFirstWeekThursday(currentThursday.year);
142+
143+
const weeksDifference = this.getWeeksDifference(currentThursday, firstWeekThursday);
144+
const weekNumber = weeksDifference + 1;
145+
146+
// Handle dates that belong to the previous year's last week
147+
if (weekNumber <= 0) {
148+
return this.getPreviousYearLastWeek(currentThursday.year - 1);
149+
}
150+
151+
return weekNumber;
152+
}
153+
154+
/**
155+
* Calculates week number using simple counting from January 1st.
156+
*/
157+
private calculateSimpleWeekNumber(weekStart: number): number {
158+
const yearStart = new CalendarDay({ year: this.year, month: 0, date: 1 });
159+
const yearStartDay = yearStart.day;
160+
161+
const daysUntilFirstWeek = (weekStart - yearStartDay + 7) % 7;
162+
163+
if (daysUntilFirstWeek > 0) {
164+
const firstWeekStart = yearStart.add('day', daysUntilFirstWeek);
165+
166+
if (this.timestamp < firstWeekStart.timestamp) {
167+
const prevYear = this.year - 1;
168+
const prevYearDec31 = new CalendarDay({ year: prevYear, month: 11, date: 31 });
169+
return prevYearDec31.calculateSimpleWeekNumber(weekStart);
170+
}
171+
172+
const daysSinceFirstWeek = Math.floor((this.timestamp - firstWeekStart.timestamp) / millisecondsInDay);
173+
return Math.floor(daysSinceFirstWeek / 7) + 1;
174+
} else {
175+
const daysSinceYearStart = Math.floor((this.timestamp - yearStart.timestamp) / millisecondsInDay);
176+
return Math.floor(daysSinceYearStart / 7) + 1;
177+
}
178+
}
179+
180+
/**
181+
* Gets the Thursday of the current date's week (ISO 8601 helper).
182+
*/
183+
private getThursdayOfWeek(): CalendarDay {
184+
const dayOffset = (this.day - 1 + 7) % 7; // Monday start
185+
const thursdayOffset = 3; // Thursday is 3 days from Monday
186+
return this.add('day', thursdayOffset - dayOffset);
187+
}
188+
189+
/**
190+
* Gets the Thursday of the first week of the given year (ISO 8601 helper).
191+
*/
192+
private getFirstWeekThursday(year: number): CalendarDay {
193+
const january4th = new CalendarDay({ year, month: 0, date: 4 });
194+
const dayOffset = (january4th.day - 1 + 7) % 7; // Monday start
195+
const thursdayOffset = 3; // Thursday is 3 days from Monday
196+
return january4th.add('day', thursdayOffset - dayOffset);
197+
}
198+
199+
/**
200+
* Calculates the number of weeks between two Thursday dates (ISO 8601 helper).
201+
*/
202+
private getWeeksDifference(currentThursday: CalendarDay, firstWeekThursday: CalendarDay): number {
203+
const daysDifference = Math.floor((currentThursday.timestamp - firstWeekThursday.timestamp) / millisecondsInDay);
204+
return Math.floor(daysDifference / 7);
205+
}
206+
207+
/**
208+
* Gets the last week number of the previous year (ISO 8601 helper).
209+
*/
210+
private getPreviousYearLastWeek(previousYear: number): number {
211+
const december31st = new CalendarDay({ year: previousYear, month: 11, date: 31 });
212+
const lastWeekThursday = december31st.getThursdayOfWeek();
213+
const firstWeekThursday = this.getFirstWeekThursday(previousYear);
214+
215+
return this.getWeeksDifference(lastWeekThursday, firstWeekThursday) + 1;
125216
}
126217

127218
/** Returns the underlying native date instance. */

projects/igniteui-angular/src/lib/calendar/days-view/days-view.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ export class IgxDaysViewComponent extends IgxCalendarBaseDirective {
378378
* @hidden
379379
*/
380380
public getWeekNumber(date: CalendarDay): number {
381-
return date.week;
381+
return date.getWeekNumber(this.weekStart);
382382
}
383383

384384
/**

src/app/calendar/calendar.sample.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,29 @@ export class CalendarSampleComponent implements OnInit {
6767
label: 'Change Locale',
6868
control: {
6969
type: 'button-group',
70-
options: ['EN', 'BG', 'DE', 'FR', 'JP'],
71-
defaultValue: 'EN'
70+
options: [
71+
{
72+
value: 'en-US',
73+
label: 'EN'
74+
},
75+
{
76+
value: 'bg-BG',
77+
label: 'BG'
78+
},
79+
{
80+
value: 'de-DE',
81+
label: 'DE'
82+
},
83+
{
84+
value: 'fr-FR',
85+
label: 'FR'
86+
},
87+
{
88+
value: 'ja-JP',
89+
label: 'JP'
90+
}
91+
],
92+
defaultValue: 'en-US'
7293
}
7394
},
7495
weekStart: {

src/app/properties-panel/properties-panel.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ <h5 class="properties-panel__title">Properties Panel</h5>
7979
<igx-buttongroup [selectionMode]="'single'">
8080
@for (option of getControlOptions(key); track option) {
8181
<button igxButton
82-
[selected]="form.controls[key].value === option"
83-
(click)="form.controls[key].setValue(option)">
82+
[selected]="form.controls[key].value === (option.value || option)"
83+
(click)="form.controls[key].setValue(option.value || option)">
8484
{{ getControlLabels(key)[getControlOptions(key).indexOf(option)] }}
8585
</button>
8686
}

src/app/properties-panel/properties-panel.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ export class PropertiesPanelComponent {
124124
return this.config[key].control.options;
125125
}
126126

127-
protected getControlLabels(key: string): string[] {
127+
protected getControlLabels(key: string): string[] | { label: string, value: any }[] {
128128
const labels = this.config[key].control.labels || [];
129129
const options = this.getControlOptions(key);
130130
return labels.length > 0
131131
? labels
132-
: options.map((option) => option.toString());
132+
: options.map((option) => option.label || option.toString());
133133
}
134134
}

src/app/properties-panel/property-change.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export type PropertyPanelConfig = {
2121
label?: string;
2222
control: {
2323
type: ControlType,
24-
options?: string[];
24+
options?: string[] | { label: string, value: any }[];
2525
labels?: string[];
2626
min?: number;
2727
max?: number;

0 commit comments

Comments
 (0)