Skip to content
nbtcaPublic

About

Calendar library for the NBTCA ICS feed

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@nbtca/nbtcal

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.

Install

npm install @nbtca/nbtcal

Usage

import { 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 };

School calendar

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 null

Personal timetable

The @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.

Personal exams and grades

@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.

Development

npm run check

License

MIT

About

Calendar library for the NBTCA ICS feed

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages