Skip to content

Introduce LessonIds - #4427

Merged
leslieyip02 merged 11 commits into
nusmodifications:masterfrom
zehata:lesson-id
Jun 21, 2026
Merged

Introduce LessonIds#4427
leslieyip02 merged 11 commits into
nusmodifications:masterfrom
zehata:lesson-id

Conversation

@zehata

@zehata zehata commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
  • Generate ModuleLessonMap after module data is fetched, which stores a map of RawLesson to their LessonId so that serializeLessonDetails only needs to be called once.
  • Generate ModuleLessonMap for sample modules as well.
  • Generate ModuleLessonMap in the export service so lessons can be rendered.
  • Currently these are inactive, and would only be used in a future PR.

Context

This is part of the changes meant to fix #4283
Please refer to #4387 for additional info

Implementation

Other than adding the types, it also generates the ModuleLessonMap when modules are downloaded. This memoizes the LessonId so that they do not have to be generated every time they are used. This also applies to the export service. This also includes a couple of helper functions for serializing and deserializing LessonIds.

LessonIds are serialization of all a lesson details, except its module info and lesson type, please refer to #4292 (comment) for why this was chosen.

        {
          "classNo": "22",
          "lessonType": "Tutorial", // not serialized
          "weeks": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
          "day": "Wednesday",
          "startTime": "0900",
          "endTime": "1000",
          "venue": "S16-0518"
        },

would serialize to
22|TUE|0900|1000|S16-0518|2_3_4_5_6_7_8_9_10_11_12_13.

Serialization and deserialization can be quite expensive. So we calculate and store a map of the LessonType to LessonIds to lessons in SemesterData.

// Semester-specific information of a module.
export type SemesterData = {
  // ...
  readonly lessonMap: LessonMap<RawLesson>;
};

Other Information

Currently these changes are inactive, they will be used in the a future PR. The delta to switch over to use LessonIds is already large enough as is.

- Generate `ModuleLessonMap` after module data is fetched, which stores a map of `RawLesson` to their `LessonId` so that serializeLessonDetails only needs to be called once.
- Generate `ModuleLessonMap` for sample modules as well.
- Generate `ModuleLessonMap` in the export service so lessons can be rendered.
- Currently these are inactive, and would only be used in a future PR.
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

@zehata is attempting to deploy a commit to the modsbot's projects Team on Vercel.

A member of the Team first needs to authorize it.

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.62887% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.76%. Comparing base (988c6fd) to head (7db8157).
⚠️ Report is 240 commits behind head on master.

Files with missing lines Patch % Lines
website/src/utils/timetables/lessonId.ts 85.36% 12 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4427      +/-   ##
==========================================
+ Coverage   54.52%   56.76%   +2.23%     
==========================================
  Files         274      317      +43     
  Lines        6076     7059     +983     
  Branches     1455     1699     +244     
==========================================
+ Hits         3313     4007     +694     
- Misses       2763     3052     +289     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@leslieyip02

Copy link
Copy Markdown
Member

@greptileai

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces LessonId as a serialization of lesson details (excluding lessonType), adds ModuleLessonMap to SemesterData, and pre-computes that map in the website reducer, mock modules, and the export service — though the maps are currently unused and gated behind a reference to a future PR.

  • Adds serializeLessonDetails, deserializeLessonDetails, parseWeeks, and related helpers in website/src/utils/timetables/lessonId.ts, with a parallel duplicate in export/src/makeModuleLessonMap.ts; makeModuleLessonMap is now called at load time in the reducer and export service so LessonIds don't need to be recomputed on every use.
  • Extends SemesterData with a required readonly lessonMap field and adds recovery helpers (getRecoveryClassNo, getRecoverySerializedLessonDetails, getClosestClassNo) for handling invalid timetable configs.
  • Updates the mock module factory and adds a new test file covering round-trip serialization and recovery behaviour.

Confidence Score: 5/5

Safe to merge — all new code paths are explicitly marked inactive and gated behind a future PR, so no changed logic runs in production today.

The serialization round-trips are well-covered by the new test suite, regex anchoring issues from earlier rounds have been addressed, and the export serializer now matches the website for empty-weeks encoding. The remaining notes are a JSDoc example with a slightly wrong serialized string and a mock factory whose lessonMap values carry lessonIndex from baked-in JSON fixtures — neither affects runtime while the feature stays inactive.

website/src/mocks/modules/index.ts — the mock does not replicate the reducer pattern of building lessonMap before adding lessonIndex, so test fixtures document a shape that differs from production.

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
website/src/__mocks__/modules/index.ts:17-24
**Mock `lessonMap` values include `lessonIndex`, production values will not**

The real reducer builds `lessonMap` from `semesterData.timetable` *before* adding `lessonIndex`, so production `lessonMap` values are plain `RawLesson` objects without `lessonIndex`. The mock skips the `lessonIndex`-augmentation step, but the JSON fixtures already have `lessonIndex` baked in (e.g. `CS3216.json` has `"lessonIndex": 0`), so `makeModuleLessonMap(semesterData.timetable)` here embeds `lessonIndex` in every map value. The test in `modules.test.ts` then asserts `lessonIndex: 0` inside the lessonMap, documenting a shape that diverges from what will be stored in production. When the map is actually consumed in a future PR, consumers relying on these test fixtures will find the values include a field the runtime won't have.

### Issue 2 of 3
website/src/utils/timetables/lessonId.ts:111-112
**`serializeWeekRange` JSDoc shows an incorrect example for empty `weeks`**

The comment claims an empty `weeks` array serializes to `'2025-01-13_2025-02-14_'`, but the actual output always includes the `weekInterval` segment (defaulting to `0`), producing `'2025-01-13_2025-02-14_0__'` (when `serializeWeekNumbers([])` returns `'_'`). The test at line 157 confirms this. The misleading example could cause confusion when hand-crafting or validating LessonIds.

```suggestion
 * If weeks is an empty array, the serialized string would be `2025-01-13_2025-02-14_0__` (weekInterval defaults to `0`, and serializeWeekNumbers([]) returns `_`)\
 * If weeks is undefined, the serialized string would be `2025-01-13_2025-02-14_0`, without the trailing separator\
```

### Issue 3 of 3
export/src/makeModuleLessonMap.ts:72-73
**Same `serializeWeekRange` JSDoc inaccuracy as in the website's `lessonId.ts`**

The description of the empty-weeks case is copied from the website code and carries the same error. The serialized output includes the `weekInterval` value before the trailing `_` separator.

```suggestion
 * If `weeks` is an empty array, the serialized string would be `2025-01-13_2025-02-14_0__` (weekInterval defaults to `0`, and serializeWeekNumbers([]) returns `_`)\
 * If `weeks` is undefined, the serialized string would be `2025-01-13_2025-02-14_0`, without the trailing separator\
```

Reviews (4): Last reviewed commit: "Modified `getRecoverySerializedLessonDet..." | Re-trigger Greptile

Comment thread website/src/utils/timetables/lessonId.ts Outdated
Comment thread website/src/utils/timetables/lessonId.ts
Comment thread website/src/utils/timetables/lessonId.ts
Comment thread website/src/utils/timetables/lessonId.ts
Comment thread export/src/data.ts Outdated
Comment thread export/src/makeModuleLessonMap.ts
@zehata

zehata commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@leslieyip02 I have made some changes, and you might want to look at the corresponding changes I made to address the issues that Greptile generated, or retrigger the review.

@leslieyip02

Copy link
Copy Markdown
Member

@greptileai

Comment thread website/src/utils/timetables/lessonId.ts
Comment thread website/src/utils/timetables/lessonId.ts
Comment thread website/src/utils/timetables/lessonId.ts
Comment thread export/src/makeModuleLessonMap.ts
Comment thread website/src/utils/timetables/lessonId.test.ts Outdated

@leslieyip02 leslieyip02 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly LGTM, just some minor nits.


describe('serialize/parse weeks', () => {
test('serialized weekRange should be deserialized to the same weekRange', () => {
const weekRanges: WeekRange[] = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add 1 more test case where with only start and end. (BME5802 is an example with just start and end in its week range).

);
};

const deserializeWeekNumbers = async (serializedWeekNumbers: string): Promise<number[]> => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be async?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't mind rewriting with try catch

* @param serializedWeeks
* @returns
*/
export const parseWeeks = async (serializedWeeks: string): Promise<Weeks> => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be async?

* @param lessonsWithLessonType lessons with the same lesson type to generate a valid lesson config from
* @returns a `ClassNo`. The current implementation generates a config containing the first `ClassNo`
*/
export function getRecoveryClassNo(lessonsWithLessonType: Record<LessonId, RawLesson>): LessonId[] {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think the return type is supposed to be ClassNo.

Also, is there a reason to return LessonId[] instead of a single nullable LessonId? The function name getRecoveryClassNo implies a single return value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, fair point there

Comment thread export/src/makeModuleLessonMap.ts Outdated
Comment on lines +68 to +69
* If weeks is an empty array, the serialized string would be `2025-01-13_2025-02-14_`\
* If weeks is undefined, the serialized string would be `2025-01-13_2025-02-14`, without the dangling `_`\

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: weeks should be enclosed by backticks (i.e. `weeks`).

Comment thread website/src/utils/timetables/lessonId.ts
@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nusmods-website Ready Ready Preview, Comment Jun 21, 2026 1:14pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
nusmods-export Ignored Ignored Preview Jun 21, 2026 1:14pm

Request Review

@leslieyip02 leslieyip02 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@leslieyip02
leslieyip02 merged commit d424d32 into nusmodifications:master Jun 21, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lessons Configs are Not Preserved When Faculty Modifies Timetable

2 participants