diff --git a/frontend/src/utils/fiscalWeek.js b/frontend/src/utils/fiscalWeek.js new file mode 100644 index 0000000..883fed3 --- /dev/null +++ b/frontend/src/utils/fiscalWeek.js @@ -0,0 +1,48 @@ +/** + * Fiscal week, as the classic ASP site computed it. + * + * That site called it "Fiscal Week" and calculated an ISO 8601 week number + * (includes/leftsidebar.asp): week 1 is the week containing 4 January, weeks + * start on Monday, and the year is decided by that week's Thursday. This is a + * faithful port - the label and the arithmetic both come from there. + * + * Worth knowing if the numbers are ever questioned: a true GE fiscal calendar + * need not line up with ISO weeks. Nobody has asked for a different rule, and + * changing it here would silently disagree with the classic site people still + * compare against, so it stays ISO until someone says otherwise. + */ + +/** + * ISO 8601 week number for a date (1-53). + * + * Works in local time deliberately: the number people quote is the one on the + * wall where they are, and a UTC-based week rolls over hours early in the + * evening at a US site. + */ +export function fiscalWeek(date = new Date()) { + const day = new Date(date.getFullYear(), date.getMonth(), date.getDate()) + + // The Thursday of this week decides which year the week belongs to, which is + // what makes late December and early January land in the right year. + const dayOfWeek = (day.getDay() + 6) % 7 // 0 = Monday + const thursday = new Date(day) + thursday.setDate(day.getDate() + 3 - dayOfWeek) + + // Week 1 is the week containing 4 January; find its Monday. + const jan4 = new Date(thursday.getFullYear(), 0, 4) + const jan4DayOfWeek = (jan4.getDay() + 6) % 7 + const week1Monday = new Date(jan4) + week1Monday.setDate(jan4.getDate() - jan4DayOfWeek) + + const days = Math.round((thursday - week1Monday) / 86400000) + return Math.floor(days / 7) + 1 +} + +/** The year the week belongs to, which is not always the calendar year. */ +export function fiscalWeekYear(date = new Date()) { + const day = new Date(date.getFullYear(), date.getMonth(), date.getDate()) + const dayOfWeek = (day.getDay() + 6) % 7 + const thursday = new Date(day) + thursday.setDate(day.getDate() + 3 - dayOfWeek) + return thursday.getFullYear() +} diff --git a/frontend/src/utils/fiscalWeek.spec.js b/frontend/src/utils/fiscalWeek.spec.js new file mode 100644 index 0000000..a65888a --- /dev/null +++ b/frontend/src/utils/fiscalWeek.spec.js @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest' +import { fiscalWeek, fiscalWeekYear } from './fiscalWeek' + +/** + * The number has to match what the classic ASP site shows, because people will + * have both open. The awkward cases are all at the year boundary, which is + * exactly where a naive "day of year / 7" gets it wrong. + */ +describe('fiscalWeek', () => { + it('counts the week containing 4 January as week 1', () => { + expect(fiscalWeek(new Date(2026, 0, 4))).toBe(1) + }) + + it('puts a late-December date in week 1 of the NEXT year', () => { + // 2025-12-29 is a Monday; its Thursday falls in 2026, so it is 2026 week 1. + expect(fiscalWeek(new Date(2025, 11, 29))).toBe(1) + expect(fiscalWeekYear(new Date(2025, 11, 29))).toBe(2026) + }) + + it('puts an early-January date in the LAST week of the previous year', () => { + // 2027-01-01 is a Friday; its Thursday is 2026-12-31, so it is 2026 week 53. + expect(fiscalWeek(new Date(2027, 0, 1))).toBe(53) + expect(fiscalWeekYear(new Date(2027, 0, 1))).toBe(2026) + }) + + it('holds the same number all week, Monday through Sunday', () => { + const monday = new Date(2026, 7, 10) + const week = fiscalWeek(monday) + for (let i = 0; i < 7; i++) { + const day = new Date(2026, 7, 10 + i) + expect(fiscalWeek(day)).toBe(week) + } + }) + + it('increments the following Monday', () => { + expect(fiscalWeek(new Date(2026, 7, 17))).toBe(fiscalWeek(new Date(2026, 7, 10)) + 1) + }) + + it('gives a 53-week year its 53rd week', () => { + // 2026 starts on a Thursday, so it runs to 53 weeks. + expect(fiscalWeek(new Date(2026, 11, 31))).toBe(53) + }) + + it('is not fooled by a time of day', () => { + const morning = new Date(2026, 7, 12, 6, 0, 0) + const nearMidnight = new Date(2026, 7, 12, 23, 59, 0) + expect(fiscalWeek(morning)).toBe(fiscalWeek(nearMidnight)) + }) +}) diff --git a/frontend/src/views/AppLayout.vue b/frontend/src/views/AppLayout.vue index bc8146d..19b4a8c 100644 --- a/frontend/src/views/AppLayout.vue +++ b/frontend/src/views/AppLayout.vue @@ -4,6 +4,11 @@