-
Notifications
You must be signed in to change notification settings - Fork 359
feat: support previous special term when academic year changes #4402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a1a52a9
feat: first draft fo ST configuration
jloh02 c5b1a37
feat: move common academic calendar to package and update maintenance…
jloh02 0714789
feat: support ST1 also for overlap
jloh02 cf81bde
Document special-term overlap behaviour flagged by Greptile review.
jloh02 3c6e39e
Merge branch 'master' into feat/st-support
jloh02 9698b8c
fix: satisfy oxlint property sort order in nus-v2 config and tasks
jloh02 7dd2cc7
Merge branch 'master' into feat/st-support
jloh02 af83906
fix: pnpm lock
jloh02 470772b
Merge branch 'master' into feat/st-support
leslieyip02 9dd1d49
chore: add unit tests for boundary values
leslieyip02 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| export type DateTuple = [number, number, number]; | ||
|
|
||
| export type AcademicCalendar = { | ||
| readonly [acadYear: string]: { | ||
| readonly [semester: string]: { readonly start: DateTuple }; | ||
| }; | ||
| }; | ||
|
|
||
| export declare const SPECIAL_TERM_SEMESTERS: readonly [3, 4]; | ||
|
|
||
| export declare const academicCalendar: AcademicCalendar; | ||
| export default academicCalendar; | ||
|
|
||
| export declare function subtractAcadYear(acadYear: string): string; | ||
|
|
||
| export declare function getSemesterStart(acadYear: string, semester: number): Date | null; | ||
|
|
||
| export declare function isPreviousAySpecialTermActive(academicYear: string, date?: Date): boolean; | ||
|
|
||
| export declare function getEffectiveSpecialTermAcadYear( | ||
| academicYear: string, | ||
| specialTermAcademicYear?: string | null, | ||
| date?: Date, | ||
| ): string; | ||
|
|
||
| export declare function isUsingPreviousAySpecialTermData( | ||
| academicYear: string, | ||
| specialTermAcademicYear?: string | null, | ||
| date?: Date, | ||
| ): boolean; | ||
|
|
||
| export declare function shouldUsePreviousAyForSemester( | ||
| semester: number, | ||
| academicYear: string, | ||
| specialTermAcademicYear?: string | null, | ||
| date?: Date, | ||
| ): boolean; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| const academicCalendar = require('./academic-calendar.json'); | ||
|
|
||
| const SPECIAL_TERM_SEMESTERS = [3, 4]; | ||
|
|
||
| function subtractAcadYear(acadYear) { | ||
| return acadYear.replace(/\d+/g, (year) => String(parseInt(year, 10) - 1)); | ||
| } | ||
|
|
||
| function getSemesterStart(acadYear, semester) { | ||
| const semesterConfig = academicCalendar[acadYear]?.[String(semester)]; | ||
| if (!semesterConfig) { | ||
| return null; | ||
| } | ||
|
|
||
| const [year, month, day] = semesterConfig.start; | ||
| return new Date(year, month - 1, day); | ||
| } | ||
|
|
||
| /** | ||
| * Returns true when previous AY Special Term I or II is still in session after | ||
| * config has switched to the new AY. Overlap runs from previous AY ST I start | ||
| * until new AY Semester 1 starts. | ||
| */ | ||
| function isPreviousAySpecialTermActive(academicYear, date = new Date()) { | ||
| const previousAcadYear = subtractAcadYear(academicYear); | ||
| const specialTermStart = getSemesterStart(previousAcadYear, 3); | ||
| const specialTermEnd = getSemesterStart(academicYear, 1); | ||
|
|
||
| if (!specialTermStart || !specialTermEnd) { | ||
| return false; | ||
| } | ||
|
|
||
| return date >= specialTermStart && date < specialTermEnd; | ||
| } | ||
|
|
||
| function getEffectiveSpecialTermAcadYear( | ||
| academicYear, | ||
| specialTermAcademicYear = null, | ||
| date = new Date(), | ||
| ) { | ||
| if (specialTermAcademicYear) { | ||
| return specialTermAcademicYear; | ||
| } | ||
|
|
||
| if (isPreviousAySpecialTermActive(academicYear, date)) { | ||
| return subtractAcadYear(academicYear); | ||
| } | ||
|
|
||
| return academicYear; | ||
| } | ||
|
|
||
| function isUsingPreviousAySpecialTermData( | ||
| academicYear, | ||
| specialTermAcademicYear = null, | ||
| date = new Date(), | ||
| ) { | ||
| return ( | ||
| getEffectiveSpecialTermAcadYear(academicYear, specialTermAcademicYear, date) !== academicYear | ||
| ); | ||
| } | ||
|
|
||
| function shouldUsePreviousAyForSemester( | ||
| semester, | ||
| academicYear, | ||
| specialTermAcademicYear = null, | ||
| date = new Date(), | ||
| ) { | ||
| if (!SPECIAL_TERM_SEMESTERS.includes(semester)) { | ||
| return false; | ||
| } | ||
|
|
||
| return isUsingPreviousAySpecialTermData(academicYear, specialTermAcademicYear, date); | ||
| } | ||
|
|
||
| module.exports = { | ||
| SPECIAL_TERM_SEMESTERS, | ||
| academicCalendar, | ||
| default: academicCalendar, | ||
| subtractAcadYear, | ||
| getSemesterStart, | ||
| isPreviousAySpecialTermActive, | ||
| getEffectiveSpecialTermAcadYear, | ||
| isUsingPreviousAySpecialTermData, | ||
| shouldUsePreviousAyForSemester, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import { | ||
| getEffectiveSpecialTermAcadYear, | ||
| isPreviousAySpecialTermActive, | ||
| isUsingPreviousAySpecialTermData, | ||
| shouldUsePreviousAyForSemester, | ||
| } from './index.js'; | ||
|
|
||
| // Refer to packages/nusmods-academic-calendar/academic-calendar.json each AY's configs. | ||
|
|
||
| describe(isPreviousAySpecialTermActive, () => { | ||
| it('returns true during previous AY Special Term I after AY migration', () => { | ||
| expect(isPreviousAySpecialTermActive('2025/2026', new Date(2025, 4, 15))).toBe(true); | ||
| expect(isPreviousAySpecialTermActive('2025/2026', new Date(2025, 4, 12))).toBe(true); | ||
| }); | ||
|
|
||
| it('returns true during previous AY Special Term II after AY migration', () => { | ||
| expect(isPreviousAySpecialTermActive('2025/2026', new Date(2025, 6, 1))).toBe(true); | ||
| expect(isPreviousAySpecialTermActive('2025/2026', new Date(2025, 7, 10))).toBe(true); | ||
| }); | ||
|
|
||
| it('returns false before previous AY Special Term I starts', () => { | ||
| expect(isPreviousAySpecialTermActive('2025/2026', new Date(2025, 4, 1))).toBe(false); | ||
| expect(isPreviousAySpecialTermActive('2025/2026', new Date(2025, 4, 11))).toBe(false); | ||
| }); | ||
|
|
||
| it('returns false after new AY semester 1 starts', () => { | ||
| expect(isPreviousAySpecialTermActive('2025/2026', new Date(2025, 7, 11))).toBe(false); | ||
| expect(isPreviousAySpecialTermActive('2025/2026', new Date(2026, 0, 1))).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe(getEffectiveSpecialTermAcadYear, () => { | ||
| it('auto-detects previous AY during overlap', () => { | ||
| expect(getEffectiveSpecialTermAcadYear('2025/2026', null, new Date(2025, 4, 15))).toBe( | ||
| '2024/2025', | ||
| ); | ||
| expect(getEffectiveSpecialTermAcadYear('2025/2026', null, new Date(2025, 6, 1))).toBe( | ||
| '2024/2025', | ||
| ); | ||
| expect(getEffectiveSpecialTermAcadYear('2025/2026', null, new Date(2025, 7, 10))).toBe( | ||
| '2024/2025', | ||
| ); | ||
| }); | ||
|
|
||
| it('uses current AY outside overlap', () => { | ||
| expect(getEffectiveSpecialTermAcadYear('2025/2026', null, new Date(2025, 7, 11))).toBe( | ||
| '2025/2026', | ||
| ); | ||
| expect(getEffectiveSpecialTermAcadYear('2025/2026', null, new Date(2025, 8, 1))).toBe( | ||
| '2025/2026', | ||
| ); | ||
| }); | ||
|
|
||
| it('uses manual override when configured', () => { | ||
| expect(getEffectiveSpecialTermAcadYear('2025/2026', '2023/2024', new Date(2025, 6, 1))).toBe( | ||
| '2023/2024', | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe(isUsingPreviousAySpecialTermData, () => { | ||
| it('returns true only during overlap', () => { | ||
| expect(isUsingPreviousAySpecialTermData('2025/2026', null, new Date(2025, 4, 15))).toBe(true); | ||
| expect(isUsingPreviousAySpecialTermData('2025/2026', null, new Date(2025, 6, 1))).toBe(true); | ||
| expect(isUsingPreviousAySpecialTermData('2025/2026', null, new Date(2025, 8, 1))).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe(shouldUsePreviousAyForSemester, () => { | ||
| it('returns true for special term semesters during overlap', () => { | ||
| expect(shouldUsePreviousAyForSemester(3, '2025/2026', null, new Date(2025, 4, 15))).toBe(true); | ||
| expect(shouldUsePreviousAyForSemester(4, '2025/2026', null, new Date(2025, 6, 1))).toBe(true); | ||
| }); | ||
|
|
||
| it('returns false for normal semesters during overlap', () => { | ||
| expect(shouldUsePreviousAyForSemester(1, '2025/2026', null, new Date(2025, 6, 1))).toBe(false); | ||
| expect(shouldUsePreviousAyForSemester(2, '2025/2026', null, new Date(2025, 6, 1))).toBe(false); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| { | ||
| "name": "nusmods-academic-calendar", | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "description": "NUS academic calendar semester start dates shared across NUSMods packages", | ||
| "license": "MIT", | ||
| "author": "NUSModifications", | ||
| "files": [ | ||
| "academic-calendar.json", | ||
| "index.js", | ||
| "index.d.ts" | ||
| ], | ||
| "main": "index.js", | ||
| "types": "index.d.ts", | ||
| "scripts": { | ||
| "test": "vitest run" | ||
| }, | ||
| "devDependencies": { | ||
| "vitest": "catalog:" | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.