handling iso date strings

This commit is contained in:
2024-08-26 13:37:23 -06:00
parent 1459655b90
commit 5d16cae417
11 changed files with 173 additions and 19 deletions

View File

@@ -0,0 +1,66 @@
interface CalendarMonth {
year: number;
month: number;
weeks: (number | null)[][];
daysByWeek: (Date | null)[][];
}
const calendarMonthUtils = {
weeksInMonth: (year: number, month: number): number => {
const firstDayOfMonth = new Date(year, month - 1, 1).getDay();
const daysInMonth = new Date(year, month, 0).getDate();
const longDaysInMonth = daysInMonth + firstDayOfMonth;
let weeks = Math.floor(longDaysInMonth / 7);
if (longDaysInMonth % 7 > 0) {
weeks += 1;
}
return weeks;
},
createCalendarMonth: (year: number, month: number): CalendarMonth => {
const daysByWeek: (Date | null)[][] = [];
const weeksInMonth = calendarMonthUtils.weeksInMonth(year, month);
const daysInMonth = new Date(year, month, 0).getDate();
let currentDay = 1;
const firstDayOfMonth = new Date(year, month - 1, 1).getDay();
for (let i = 0; i < weeksInMonth; i++) {
const thisWeek: (Date | null)[] = [];
if (i === 0 && firstDayOfMonth !== 0) { // 0 is Sunday in JavaScript
for (let j = 0; j < 7; j++) {
if (j < firstDayOfMonth) {
thisWeek.push(null);
} else {
thisWeek.push(new Date(year, month - 1, currentDay));
currentDay++;
}
}
} else {
for (let j = 0; j < 7; j++) {
if (currentDay <= daysInMonth) {
thisWeek.push(new Date(year, month - 1, currentDay));
currentDay++;
} else {
thisWeek.push(null);
}
}
}
daysByWeek.push(thisWeek);
}
const weeks = daysByWeek.map(week => week.map(day => day ? day.getDate() : null));
return { year, month, weeks, daysByWeek };
},
getMonthsBetweenDates: (startDate: Date, endDate: Date): CalendarMonth[] => {
const monthsInTerm = 1 + (endDate.getFullYear() - startDate.getFullYear()) * 12 + endDate.getMonth() - startDate.getMonth();
return Array.from({ length: monthsInTerm }, (_, monthDiff) => {
const month = ((startDate.getMonth() + monthDiff) % 12) + 1;
const year = startDate.getFullYear() + Math.floor((startDate.getMonth() + monthDiff) / 12);
return calendarMonthUtils.createCalendarMonth(year, month);
});
}
};

View File

@@ -0,0 +1,27 @@
"use client";
import { useLocalCourseDetailsQuery } from "@/hooks/localCoursesHooks";
import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
export default function Page({
params: { courseName },
}: {
params: { courseName: string };
}) {
const { data: course } = useLocalCourseDetailsQuery(courseName);
console.log(course);
const startDate = getDateFromStringOrThrow(course.settings.startDate);
const endDate = getDateFromStringOrThrow(course.settings.endDate);
const months = calendarMonthUtils.getMonthsBetweenDates(startDate, endDate);
return (
<div>
{course.settings.name}
<div>
{months.map((month) => (
<div key={month.month + "" + month.year}>{month.month}</div>
))}
</div>
</div>
);
}

View File

@@ -4,7 +4,6 @@ import "./globals.css";
import { dehydrate } from "@tanstack/react-query"; import { dehydrate } from "@tanstack/react-query";
import { MyQueryClientProvider } from "@/services/utils/MyQueryClientProvider"; import { MyQueryClientProvider } from "@/services/utils/MyQueryClientProvider";
import { hydrateCourses } from "@/hooks/hookHydration"; import { hydrateCourses } from "@/hooks/hookHydration";
import { LoadingAndErrorHandling } from "@/components/LoadingAndErrorHandling";
import { createQueryClientForServer } from "@/services/utils/queryClientServer"; import { createQueryClientForServer } from "@/services/utils/queryClientServer";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@@ -31,9 +30,9 @@ export default async function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<MyQueryClientProvider dehydratedState={dehydratedState}> <MyQueryClientProvider dehydratedState={dehydratedState}>
<LoadingAndErrorHandling> {/* <LoadingAndErrorHandling> */}
<body className={inter.className}>{children}</body> <body className={inter.className}>{children}</body>
</LoadingAndErrorHandling> {/* </LoadingAndErrorHandling> */}
</MyQueryClientProvider> </MyQueryClientProvider>
</html> </html>
); );

View File

@@ -1,12 +1,13 @@
"use client" "use client"
import { useLocalCoursesQuery } from "@/hooks/localCoursesHooks"; import { useLocalCoursesQuery } from "@/hooks/localCoursesHooks";
import Link from "next/link";
export default function Home() { export default function Home() {
const { data: courses } = useLocalCoursesQuery(); const { data: courses } = useLocalCoursesQuery();
return ( return (
<main className="flex min-h-screen flex-col items-center justify-between p-24"> <main className="flex min-h-screen flex-col items-center justify-between p-24">
{courses.map((c) => ( {courses.map((c) => (
<div key={c.settings.name}>{c.settings.name} </div> <Link href={`/course/${c.settings.name}`} key={c.settings.name}>{c.settings.name} </Link>
))} ))}
</main> </main>
); );

View File

@@ -2,6 +2,7 @@ import { QueryErrorResetBoundary } from "@tanstack/react-query";
import { FC, ReactNode, Suspense } from "react"; import { FC, ReactNode, Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
// not at top level?
export const LoadingAndErrorHandling: FC<{ children: ReactNode }> = ({ export const LoadingAndErrorHandling: FC<{ children: ReactNode }> = ({
children, children,
}) => { }) => {

View File

@@ -4,6 +4,7 @@ import axios from "axios";
export const localCourseKeys = { export const localCourseKeys = {
allCourses: ["all courses"] as const, allCourses: ["all courses"] as const,
courseDetail: (courseName: string) => ["all courses", courseName] as const,
}; };
export const useLocalCoursesQuery = () => export const useLocalCoursesQuery = () =>
@@ -15,3 +16,19 @@ export const useLocalCoursesQuery = () =>
return response.data; return response.data;
}, },
}); });
export const useLocalCourseDetailsQuery = (courseName: string) => {
const { data: courses } = useLocalCoursesQuery();
return useSuspenseQuery({
queryKey: localCourseKeys.courseDetail(courseName),
queryFn: () => {
const course = courses.find((c) => c.settings.name === courseName);
if (!course) {
console.log(courses);
console.log(courseName);
throw Error(`Could not find course with name ${courseName}`);
}
return course;
},
});
};

View File

@@ -33,9 +33,32 @@ export enum DayOfWeek {
} }
export const localCourseYamlUtils = { export const localCourseYamlUtils = {
parseSettingYaml: (settingsString: string): LocalCourseSettings => { parseSettingYaml: (settingsString: string): LocalCourseSettings => {
return parse(settingsString); const settings = parse(settingsString);
return lowercaseFirstLetter(settings)
}, },
settingsToYaml: (settings: LocalCourseSettings) => { settingsToYaml: (settings: LocalCourseSettings) => {
return stringify(settings); return stringify(settings);
}, },
}; };
function lowercaseFirstLetter<T>(obj: T): T {
if (obj === null || typeof obj !== 'object')
return obj as T;
if (Array.isArray(obj))
return obj.map(lowercaseFirstLetter) as unknown as T;
const result: Record<string, any> = {};
Object.keys(obj).forEach((key) => {
const value = (obj as Record<string, any>)[key];
const newKey = key.charAt(0).toLowerCase() + key.slice(1);
if (value && typeof value === 'object') {
result[newKey] = lowercaseFirstLetter(value);
} else {
result[newKey] = value;
}
});
return result as T;
}

View File

@@ -15,4 +15,9 @@ describe("Can properly handle expected date formats", () => {
const dateObject = getDateFromString(dateString) const dateObject = getDateFromString(dateString)
expect(dateObject).not.toBeUndefined() expect(dateObject).not.toBeUndefined()
}) })
it("can use ISO format", () =>{
const dateString = "2024-08-26T00:00:00.0000000"
const dateObject = getDateFromString(dateString)
expect(dateObject).not.toBeUndefined()
})
}) })

View File

@@ -1,5 +1,7 @@
const _getDateFromAMPM = (
const _getDateFromAMPM = (datePart: string, timePartWithMeridian: string): Date | undefined => { datePart: string,
timePartWithMeridian: string
): Date | undefined => {
const [day, month, year] = datePart.split("/").map(Number); const [day, month, year] = datePart.split("/").map(Number);
const [timePart, meridian] = timePartWithMeridian.split(" "); const [timePart, meridian] = timePartWithMeridian.split(" ");
const [hours, minutes, seconds] = timePart.split(":").map(Number); const [hours, minutes, seconds] = timePart.split(":").map(Number);
@@ -18,7 +20,10 @@ const _getDateFromAMPM = (datePart: string, timePartWithMeridian: string): Date
return isNaN(date.getTime()) ? undefined : date; return isNaN(date.getTime()) ? undefined : date;
}; };
const _getDateFromMilitary = (datePart: string, timePart: string): Date | undefined => { const _getDateFromMilitary = (
datePart: string,
timePart: string
): Date | undefined => {
const [day, month, year] = datePart.split("/").map(Number); const [day, month, year] = datePart.split("/").map(Number);
const [hours, minutes, seconds] = timePart.split(":").map(Number); const [hours, minutes, seconds] = timePart.split(":").map(Number);
@@ -26,14 +31,20 @@ const _getDateFromMilitary = (datePart: string, timePart: string): Date | undefi
return isNaN(date.getTime()) ? undefined : date; return isNaN(date.getTime()) ? undefined : date;
}; };
const _getDateFromISO = (value: string): Date | undefined => {
const date = new Date(value);
return isNaN(date.getTime()) ? undefined : date;
};
export const getDateFromString = (value: string): Date | undefined => { export const getDateFromString = (value: string): Date | undefined => {
// Regex for AM/PM format: "M/D/YYYY h:mm:ss AM/PM" const ampmDateRegex =
const ampmDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}\s{1}[APap][Mm]$/; /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}\s{1}[APap][Mm]$/; //"M/D/YYYY h:mm:ss AM/PM"
const militaryDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}$/; //"MM/DD/YYYY HH:mm:ss"
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)$/; //"2024-08-26T00:00:00.0000000"
// Regex for military time format: "MM/DD/YYYY HH:mm:ss" if (isoDateRegex.test(value)) {
const militaryDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}$/; return _getDateFromISO(value);
} else if (ampmDateRegex.test(value)) {
if (ampmDateRegex.test(value)) {
const [datePart, timePartWithMeridian] = value.split(/[\s\u202F]+/); const [datePart, timePartWithMeridian] = value.split(/[\s\u202F]+/);
return _getDateFromAMPM(datePart, timePartWithMeridian); return _getDateFromAMPM(datePart, timePartWithMeridian);
} else if (militaryDateRegex.test(value)) { } else if (militaryDateRegex.test(value)) {
@@ -45,7 +56,11 @@ export const getDateFromString = (value: string): Date | undefined => {
} }
}; };
export const getDateFromStringOrThrow = (value: string): Date => {
const d = getDateFromString(value);
if (!d) throw Error(`Invalid date format, ${value}`);
return d;
};
export const verifyDateStringOrUndefined = ( export const verifyDateStringOrUndefined = (
value: string value: string

View File

@@ -73,6 +73,7 @@ export const courseMarkdownLoader = {
const settingsString = await fs.readFile(settingsPath, "utf-8"); const settingsString = await fs.readFile(settingsPath, "utf-8");
const settings = localCourseYamlUtils.parseSettingYaml(settingsString); const settings = localCourseYamlUtils.parseSettingYaml(settingsString);
console.log(settingsString, settings);
const folderName = path.basename(courseDirectory); const folderName = path.basename(courseDirectory);
return { ...settings, name: folderName }; return { ...settings, name: folderName };

View File

@@ -3,7 +3,6 @@
import { import {
DehydratedState, DehydratedState,
hydrate, hydrate,
HydrationBoundary,
QueryClientProvider, QueryClientProvider,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import React from "react"; import React from "react";
@@ -16,9 +15,9 @@ export const MyQueryClientProvider: FC<{
}> = ({ children, dehydratedState }) => { }> = ({ children, dehydratedState }) => {
const [queryClient] = useState(createQueryClient()); const [queryClient] = useState(createQueryClient());
hydrate(queryClient, dehydratedState);
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
<HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
</QueryClientProvider>
); );
}; };