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.
npm run checkMIT