feat(First-day-of-the-week and date-notes)!: make any day configurable to become first day of the week and make date-notes ISO 8601-compliant

Section 4.1.4.1 ISO 8601-1
https://www.loc.gov/standards/datetime/iso-tc154-wg5_n0038_iso_wd_8601-1_2016-02-16.pdf
This commit is contained in:
Jakob Schlanstedt 2025-10-09 05:14:57 +02:00 committed by Jakob Schlanstedt
parent e1a0452448
commit 89879a6851
3 changed files with 27 additions and 84 deletions

View File

@ -1415,8 +1415,13 @@
"title": "Localization",
"language": "Language",
"first-day-of-the-week": "First day of the week",
"sunday": "Sunday",
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"sunday": "Sunday",
"first-week-of-the-year": "First week of the year",
"first-week-contains-first-day": "First week contains first day of the year",
"first-week-contains-first-thursday": "First week contains first Thursday of the year",

View File

@ -73,13 +73,21 @@ function DateSettings() {
return (
<>
<OptionsRow name="first-day-of-week" label={t("i18n.first-day-of-the-week")}>
<FormInlineRadioGroup
<FormSelect
name="first-day-of-week"
currentValue={firstDayOfWeek}
onChange={setFirstDayOfWeek}
keyProperty="value"
titleProperty="label"
values={[
{ value: "0", label: t("i18n.sunday") },
{ value: "1", label: t("i18n.monday") }
{ value: "1", label: t("i18n.monday") },
{ value: "2", label: t("i18n.tuesday") },
{ value: "3", label: t("i18n.wednesday") },
{ value: "4", label: t("i18n.thursday") },
{ value: "5", label: t("i18n.friday") },
{ value: "6", label: t("i18n.saturday") },
{ value: "7", label: t("i18n.sunday") },
]}
currentValue={firstDayOfWeek} onChange={setFirstDayOfWeek}
/>
</OptionsRow>

View File

@ -16,10 +16,12 @@ import searchService from "../services/search/services/search.js";
import sql from "./sql.js";
import { t } from "i18next";
import { ordinal } from "./i18n.js";
import isoWeek from "dayjs/plugin/isoWeek.js";
dayjs.extend(isSameOrAfter);
dayjs.extend(quarterOfYear);
dayjs.extend(advancedFormat);
dayjs.extend(isoWeek);
const CALENDAR_ROOT_LABEL = "calendarRoot";
const YEAR_LABEL = "yearNote";
@ -295,88 +297,16 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
}
function getWeekStartDate(date: Dayjs): Dayjs {
const day = date.day();
let diff: number;
if (optionService.getOption("firstDayOfWeek") === "0") { // Sunday
diff = date.date() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
} else { // Monday
diff = date.date() - day;
}
const startDate = date.clone().date(diff);
return startDate;
const firstDayISO = parseInt(optionService.getOptionOrNull("firstDayOfWeek") ?? "1", 10);
const day = date.isoWeekday();
const diff = (day - firstDayISO + 7) % 7;
return date.clone().subtract(diff, "day").startOf("day");
}
// TODO: Duplicated with getWeekNumber in src/public/app/widgets/buttons/calendar.ts
// Maybe can be merged later in monorepo setup
function getWeekNumberStr(date: Dayjs): string {
const year = date.year();
const dayOfWeek = (day: number) =>
(day - parseInt(optionService.getOption("firstDayOfWeek")) + 7) % 7;
// Get first day of the year and adjust to first week start
const jan1 = date.clone().year(year).month(0).date(1);
const jan1Weekday = jan1.day();
const dayOffset = dayOfWeek(jan1Weekday);
let firstWeekStart = jan1.clone().subtract(dayOffset, "day");
// Adjust based on week rule
switch (parseInt(optionService.getOption("firstWeekOfYear"))) {
case 1: { // ISO 8601: first week contains Thursday
const thursday = firstWeekStart.clone().add(3, "day"); // Monday + 3 = Thursday
if (thursday.year() < year) {
firstWeekStart = firstWeekStart.add(7, "day");
}
break;
}
case 2: { // minDaysInFirstWeek rule
const daysInFirstWeek = 7 - dayOffset;
if (daysInFirstWeek < parseInt(optionService.getOption("minDaysInFirstWeek"))) {
firstWeekStart = firstWeekStart.add(7, "day");
}
break;
}
// default case 0: week containing Jan 1 → already handled
}
const diffDays = date.startOf("day").diff(firstWeekStart.startOf("day"), "day");
const weekNumber = Math.floor(diffDays / 7) + 1;
// Handle case when date is before first week start → belongs to last week of previous year
if (weekNumber <= 0) {
return getWeekNumberStr(date.subtract(1, "day"));
}
// Handle case when date belongs to first week of next year
const nextYear = year + 1;
const jan1Next = date.clone().year(nextYear).month(0).date(1);
const jan1WeekdayNext = jan1Next.day();
const offsetNext = dayOfWeek(jan1WeekdayNext);
let nextYearWeekStart = jan1Next.clone().subtract(offsetNext, "day");
switch (parseInt(optionService.getOption("firstWeekOfYear"))) {
case 1: {
const thursday = nextYearWeekStart.clone().add(3, "day");
if (thursday.year() < nextYear) {
nextYearWeekStart = nextYearWeekStart.add(7, "day");
}
break;
}
case 2: {
const daysInFirstWeek = 7 - offsetNext;
if (daysInFirstWeek < parseInt(optionService.getOption("minDaysInFirstWeek"))) {
nextYearWeekStart = nextYearWeekStart.add(7, "day");
}
break;
}
}
if (date.isSameOrAfter(nextYearWeekStart)) {
return `${nextYear}-W01`;
}
return `${year}-W${weekNumber.toString().padStart(2, "0")}`;
const isoYear = date.isoWeekYear();
const isoWeekNum = date.isoWeek();
return `${isoYear}-W${isoWeekNum.toString().padStart(2, "0")}`;
}
function getWeekFirstDayNote(dateStr: string, rootNote: BNote | null = null) {