Typed ESM library for three NBTCA calendars: the club events feed, the school calendar, and a student's timetable from the campus system. It parses feeds, expands recurring events, answers date queries, builds heatmaps, and writes ICS. It never handles credentials, sessions, files, or UI.
npm install @nbtca/nbtcalimport { loadCalendar } from '@nbtca/nbtcal';
const calendar = await loadCalendar();
const start = new Date('2026-09-01T00:00:00Z');
const end = new Date('2026-10-01T00:00:00Z');
const upcoming = calendar.upcoming({ days: 30 });
const nextFive = calendar.next(5);
const range = calendar.inRange(start, end);
const dailyCounts = calendar.heatmap({ start, end, bucket: 'day' });Recurring events are expanded within each query window. Heatmaps include zero-count buckets. To skip downloading an unchanged feed, send back the validators from the last response:
import { fetchFeedConditional } from '@nbtca/nbtcal';
const result = await fetchFeedConditional(undefined, { validators: saved.validators });
const text = result.status === 'modified' ? result.text : saved.text;
saved = { text, validators: result.validators };The school calendar is a separate feed. Pass its events to the academic helpers to get the current term, week, or break:
import {
SCHOOL_FEED_URL,
loadCalendar,
currentAcademicWindow,
inferWeekOneMonday,
} from '@nbtca/nbtcal';
const school = await loadCalendar({ url: SCHOOL_FEED_URL });
const now = new Date();
const span = 400 * 86400000;
const events = school.inRange(new Date(now.getTime() - span), new Date(now.getTime() + span));
const window = currentAcademicWindow(events, now); // inTerm, onBreak, or null
const weekOne = inferWeekOneMonday(events, now); // 'YYYY-MM-DD' or nullThe @nbtca/nbtcal/timetable subpath accepts an authenticated transport for the
campus JWXT protocol:
import {
createNbtTimetableClient,
createTimetableSchedule,
findAcademicTerm,
timetableToIcs,
} from '@nbtca/nbtcal/timetable';
const client = createNbtTimetableClient(authenticatedTransport, {
baseUrl: 'https://jwxt-443.webvpn.nbt.edu.cn',
});
const terms = await client.listTerms();
const current = findAcademicTerm(terms);
if (!current) throw new Error('No current academic term');
const timetable = await client.fetchTerm(current);
const schedule = createTimetableSchedule(timetable, { weekOneMonday: '2026-09-07' });
const week = schedule.weekAt(new Date());
const today = schedule.meetingsOnDay(week, schedule.weekdayAt(new Date()));
const nextClass = schedule.next();
const ics = timetableToIcs(timetable, {
// Confirm this date for the selected term.
weekOneMonday: '2026-09-07',
});findAcademicTerm accepts an opaque year:code selector or the year-1,
year-2, and year-3 semester aliases.
The host injects the authenticated transport, so this package never sees
credentials or cookies. Pass weekOneMonday when JWXT omits term dates;
inferWeekOneMonday can supply it from the school calendar. Malformed rows
produce structured warnings, and unresolved practice rows keep only
allowlisted, non-identifying fields.
Schedules run on campus time (Asia/Shanghai) whatever the host zone.
campusIsoDate(date), campusDateTime('2026-09-07', '08:00') and
campusWeekday(date) convert between instants and campus dates.
@nbtca/nbtcal/academic uses the same host-provided authenticated transport as
the timetable API. It queries one academic term, follows pagination sequentially,
and returns only allowlisted fields. It never accepts credentials or stores results.
import {
createNbtStudentRecordsClient,
createStudentExamCalendar,
studentExamsToIcs,
} from '@nbtca/nbtcal/academic';
const recordsClient = createNbtStudentRecordsClient(authenticatedTransport, {
baseUrl: 'https://jwxt-443.webvpn.nbt.edu.cn',
});
const exams = await recordsClient.fetchRecords('exams', current);
const grades = await recordsClient.fetchRecords('grades', current);
const { events, unscheduled } = createStudentExamCalendar(exams);
if (unscheduled.length === 0) {
const ics = studentExamsToIcs(exams);
}events uses the shared CalendarEvent model and contains only exams with
published start and end times, interpreted in Asia/Shanghai. Exams without a
usable time remain in unscheduled; they never become all-day or guessed events.
The ICS exporter rejects incomplete schedules rather than silently omitting exams.
Its events use CLASS:PRIVATE and stable, identity-free UIDs. Room and time
corrections keep the UID of a uniquely identified course/exam combination. When
the school supplies multiple indistinguishable exams for that combination, their
UIDs follow chronological order.
Grades retain the school's numeric or textual result, credits and grade point.
The library does not calculate an overall GPA or infer pass/fail thresholds.
Hosts must escape returned text for their output context, including terminals.
STUDENT_RECORD_ROUTES exposes the exact academic routes so hosts can restrict
their transport. Errors expose a StudentRecordsError code without raw responses.
npm run checkMIT