TRF is a TypeScript parser and serializer for the FIDE Tournament Report File format — the standard interchange format used by all FIDE-endorsed pairing software (JaVaFo, bbpPairings, Swiss Manager, Vega).
Parses TRF strings into a fully-typed Tournament object and serializes them
back. Zero runtime dependencies.
npm install @echecs/trfimport { parse, stringify } from '@echecs/trf';
const tournament = parse(trfString);
console.log(tournament.name); // "My Tournament"
console.log(tournament.rounds); // 9
console.log(tournament.players[0].name); // "Player0001"
console.log(tournament.players[0].rating); // 2720
console.log(tournament.players[0].results); // [{ round: 1, color: 'w', opponentId: 4, result: '1' }, ...]
const trf = stringify(tournament); // back to TRF stringimport { parse } from '@echecs/trf';
function parse(input: string, options?: ParseOptions): Tournament | null;Takes a TRF string and returns a Tournament object, or null if the input
cannot be parsed.
- Strips BOM and surrounding whitespace automatically.
- Never throws — parse failures call
options.onErrorand returnnull. - Recoverable issues (unknown tags, malformed fields) call
options.onWarningand continue parsing.
import { parse } from '@echecs/trf';
const tournament = parse(trfString, {
onError: (err) => console.error(`Parse failed: ${err.message}`),
onWarning: (warn) => console.warn(`Warning: ${warn.message}`),
});Round results on each player use the following codes:
| Code | Meaning |
|---|---|
1 |
Win |
0 |
Loss |
= |
Draw |
+ |
Forfeit win |
- |
Forfeit loss |
D |
Draw (unrated game, less than one move) |
F |
Full-point bye |
H |
Half-point bye |
L |
Loss (unrated game, less than one move) |
U |
Unplayed |
W |
Win (unrated game, less than one move) |
Z |
Zero-point bye |
import { stringify } from '@echecs/trf';
function stringify(tournament: Tournament, options?: StringifyOptions): string;Takes a Tournament object and returns a TRF string.
- Never throws.
- Omits optional header fields when absent.
parse(stringify(t))roundtrips cleanly for any validTournament.- Warns (via
options.onWarning) when a player string field exceeds its column width and will be truncated.
import { parse, stringify } from '@echecs/trf';
const t1 = parse(trfString)!;
// ...modify t1...
const updated = stringify(t1, {
onWarning: (w) => console.warn(w.message),
});@echecs/trf has no dependency on @echecs/swiss by design. To use a parsed
tournament as input to the Swiss pairing functions, adapt the types in your own
code:
import { parse } from '@echecs/trf';
import { pair } from '@echecs/swiss';
import type { Tournament } from '@echecs/trf';
import type { Game, Player } from '@echecs/swiss';
function toPlayers(tournament: Tournament): Player[] {
return tournament.players.map((p) => ({
id: String(p.pairingNumber),
rating: p.rating,
}));
}
function toGames(tournament: Tournament): Game[][] {
const gamesByRound = new Map<number, Game[]>();
for (const player of tournament.players) {
for (const result of player.results) {
if (result.color !== 'w' || result.opponentId === null) continue;
let score: 0 | 0.5 | 1;
if (result.result === '1' || result.result === '+') score = 1;
else if (result.result === '0' || result.result === '-') score = 0;
else if (result.result === '=') score = 0.5;
else continue;
const games = gamesByRound.get(result.round) ?? [];
games.push({
black: String(result.opponentId),
result: score,
white: String(player.pairingNumber),
});
gamesByRound.set(result.round, games);
}
}
const roundCount = Math.max(0, ...gamesByRound.keys());
return Array.from(
{ length: roundCount },
(_, i) => gamesByRound.get(i + 1) ?? [],
);
}
const tournament = parse(trfString)!;
const pairings = pair(toPlayers(tournament), toGames(tournament));interface Tournament {
abnormalPoints?: AbnormalPoints[]; // Tag 299 — abnormal result overrides per round
absentPlayers?: number[]; // XXZ — pairing numbers absent for current round
acceleratedRounds?: AcceleratedRound[]; // Tag 250 — per-player fictitious points per round range
byes?: Bye[]; // Tag 240 — bye assignments per round
chiefArbiter?: string; // Tag 102
city?: string;
colourSequence?: string; // Tag 352 — e.g. 'WBWBWB'
comments?: string[]; // TRF26 '###' comment lines
deputyArbiters?: string[]; // Tag 112 — one entry per deputy arbiter line
encodedTimeControl?: string; // Tag 222 — e.g. '5400+30'
encodedTournamentType?: string; // Tag 192 — e.g. 'FIDE_DUTCH_2025'
endDate?: string;
federation?: string;
forfeitedMatches?: ForfeitedMatch[]; // Tag 330 — forfeited team matches per round
initialColour?: 'B' | 'W'; // Tag 152 / XXC white1/black1
name?: string;
numberOfPlayers?: number; // Tag 062
numberOfRatedPlayers?: number; // Tag 072
numberOfTeams?: number; // Tag 082
outOfOrderLineups?: OutOfOrderLineup[]; // Tag 300 — out-of-order team lineups per round
pairingController?: string; // Tag 092
playerAccelerations?: PlayerAcceleration[]; // XXA — per-player acceleration points
players: Player[];
prohibitedPairings?: ProhibitedPairing[]; // Tag 260 / XXP — forbidden pairings
roundDates?: string[]; // Tag 132 — one ISO date per round
rounds: number; // XXR — total planned round count
scoringSystem?: ScoringSystem; // Tag 162 / XXS
standingsTiebreaks?: string[]; // Tag 212 — codes for defining standings
startDate?: string;
startingRankMethod?: string; // Tag 172 — e.g. 'FRA FIDON'
teamPairingAllocatedByes?: TeamPairingAllocatedBye; // Tag 320
teams?: Team[]; // Tag 310
teamScoringSystem?: string; // Tag 362 — e.g. 'TW 2.0 TD 1.0 TL 0.0'
tiebreaks?: string[]; // Tag 202 — codes for breaking ties
timeControl?: string; // Tag 122
tournamentType?: string; // Tag 092 (TRF26) — free-form tournament type
useRankingId?: boolean; // XXC rank
version: Version; // 'TRF16' | 'TRF26'
}interface Player {
birthDate?: string;
federation?: string;
fideId?: string;
name: string;
nationalRatings?: NationalRating[]; // NRS records for this player
pairingNumber: number;
points: number;
rank: number;
rating?: number;
results: RoundResult[];
sex?: Sex; // 'm' | 'w'
title?: Title; // 'GM' | 'IM' | 'FM' | ...
}interface RoundResult {
color: 'b' | 'w' | '-'; // '-' = no color assigned (bye/unplayed)
opponentId: number | null; // null for byes
result: ResultCode;
round: number;
}National Rating Support (NRS) record, attached to a player via
Player.nationalRatings.
interface NationalRating {
birthDate?: string;
classification?: string;
federation: string; // issuing federation code
name?: string;
nationalId?: string;
origin?: string;
pairingNumber: number;
rating: number;
sex?: Sex;
}interface Team {
gamePoints: number;
matchPoints: number;
name: string;
nickname?: string;
pairingNumber: number;
playerIds: number[]; // ordered list of player pairing numbers
rank: number;
strengthFactor?: number;
}Fictitious points awarded to a player over a range of rounds (Tag 250).
interface AcceleratedRound {
firstPlayerId: number;
firstRound: number;
gamePoints: number;
lastPlayerId: number;
lastRound: number;
matchPoints: number;
}Abnormal result override for a group of players in a given round (Tag 299).
interface AbnormalPoints {
gamePoints: number;
matchPoints: number;
playerIds: number[];
round: number;
type: ' ' | '+' | '-' | 'D' | 'F' | 'H' | 'L' | 'W' | 'Z';
}Bye assignment for a group of players in a given round (Tag 240).
interface Bye {
playerIds: number[];
round: number;
type: 'F' | 'H' | 'Z';
}Forfeited team match in a given round (Tag 330).
interface ForfeitedMatch {
blackTeamId: number;
round: number;
type: '--' | '-+' | '+-'; // '--' both forfeit, '-+' black wins, '+-' white wins
whiteTeamId: number;
}Out-of-order team lineup against an opponent in a given round (Tag 300).
interface OutOfOrderLineup {
opponentTeamId: number;
playerIds: (number | null)[]; // null = board not filled
round: number;
teamId: number;
}A pairing forbidden between two players over a round range (Tag 260 / XXP).
interface ProhibitedPairing {
firstRound: number; // 0 = applies to all rounds (XXP style)
lastRound: number;
playerIds: number[]; // exactly two player pairing numbers
}Team pairing-allocated bye record (Tag 320).
interface TeamPairingAllocatedBye {
gamePoints: number;
matchPoints: number;
teamIdPerRound: (number | null)[]; // indexed by round; null = no bye that round
}Per-player acceleration record (XXA). Stores fictitious extra points per round.
interface PlayerAcceleration {
pairingNumber: number;
points: number[]; // one value per round, indexed from 0
}Custom scoring weights for result types (Tag 162 / XXS).
interface ScoringSystem {
absence?: number;
blackDraw?: number;
blackLoss?: number;
blackWin?: number;
draw?: number;
forfeitLoss?: number;
forfeitWin?: number;
fullPointBye?: number;
halfPointBye?: number;
loss?: number;
pairingAllocatedBye?: number;
unknown?: number;
whiteDraw?: number;
whiteLoss?: number;
whiteWin?: number;
win?: number;
zeroPointBye?: number;
}interface ParseOptions {
onError?: (error: ParseError) => void;
onWarning?: (warning: ParseWarning) => void;
}Reported via ParseOptions.onError when parsing fails unrecoverably. When an
error is reported, parse() returns null.
interface ParseError {
column: number; // 1-based column in the source
line: number; // 1-based line in the source
message: string;
offset: number; // byte offset in the source
}Reported via ParseOptions.onWarning (or StringifyOptions.onWarning) for
recoverable issues. Parsing continues after a warning.
interface ParseWarning {
column: number; // 1-based column in the source
line: number; // 1-based line in the source (player index for stringify)
message: string;
offset: number; // byte offset in the source (0 for stringify)
}interface StringifyOptions {
onWarning?: (warning: ParseWarning) => void;
}type ResultCode =
| '+' // forfeit win
| '-' // forfeit loss
| '0' // loss
| '1' // win
| '=' // draw
| 'D' // draw (unrated game, less than one move)
| 'F' // full-point bye
| 'H' // half-point bye
| 'L' // loss (unrated game, less than one move)
| 'U' // unplayed
| 'W' // win (unrated game, less than one move)
| 'Z'; // zero-point byetype Sex = 'm' | 'w';FIDE title codes.
type Title = 'CM' | 'FM' | 'GM' | 'IM' | 'WCM' | 'WFM' | 'WGM' | 'WIM';type Version = 'TRF16' | 'TRF26';| Format | Status | Description |
|---|---|---|
| TRF16 | Full | FIDE TRF standard (2016) |
| TRF26 | Full | FIDE TRF standard (2026), all tags including 162, 192, 172, 222, 352, 362 |
| TRFx | Full | JaVaFo extensions (XXC, XXZ, XXP, XXA, XXS) |
The Tournament Report File (TRF) format is defined in the FIDE Handbook. TRF16 is the 2016 standard; TRF26 was approved by FIDE Council on 12/05/2025 and applied from 01/09/2025. TRFx is the de facto extension format used by JaVaFo, the FIDE reference pairing engine.
MIT