{"id":51319233,"url":"https://github.com/echecsjs/trf","last_synced_at":"2026-07-01T11:02:07.791Z","repository":{"id":345017208,"uuid":"1184106828","full_name":"echecsjs/trf","owner":"echecsjs","description":"FIDE Tournament Report File (TRF) parser.","archived":false,"fork":false,"pushed_at":"2026-06-30T13:07:49.000Z","size":925,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-30T15:11:08.203Z","etag":null,"topics":["chess","fide","parser","trf","typescript"],"latest_commit_sha":null,"homepage":"https://trf.echecs.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/echecsjs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-17T09:01:30.000Z","updated_at":"2026-06-30T13:07:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/echecsjs/trf","commit_stats":null,"previous_names":["mormubis/trf","echecsjs/trf"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/echecsjs/trf","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/echecsjs%2Ftrf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/echecsjs%2Ftrf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/echecsjs%2Ftrf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/echecsjs%2Ftrf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/echecsjs","download_url":"https://codeload.github.com/echecsjs/trf/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/echecsjs%2Ftrf/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35003464,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chess","fide","parser","trf","typescript"],"created_at":"2026-07-01T11:02:06.679Z","updated_at":"2026-07-01T11:02:07.785Z","avatar_url":"https://github.com/echecsjs.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TRF\n\n[![npm](https://img.shields.io/npm/v/@echecs/trf)](https://www.npmjs.com/package/@echecs/trf)\n[![Coverage](https://codecov.io/gh/echecsjs/trf/branch/main/graph/badge.svg)](https://codecov.io/gh/echecsjs/trf)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Spec](https://img.shields.io/badge/Spec-FIDE-green.svg)](SPEC.md)\n\n**TRF** is a TypeScript parser and serializer for the\n[FIDE Tournament Report File](https://handbook.fide.com/files/handbook/TRF26.pdf)\nformat — the standard interchange format used by all FIDE-endorsed pairing\nsoftware (JaVaFo, bbpPairings, Swiss Manager, Vega).\n\nParses TRF strings into a fully-typed `Tournament` object and serializes them\nback. Zero runtime dependencies.\n\n## Installation\n\n```bash\nnpm install @echecs/trf\n```\n\n## Quick Start\n\n```typescript\nimport { parse, stringify } from '@echecs/trf';\n\nconst tournament = parse(trfString);\n\nconsole.log(tournament.name); // \"My Tournament\"\nconsole.log(tournament.rounds); // 9\nconsole.log(tournament.players[0].name); // \"Player0001\"\nconsole.log(tournament.players[0].rating); // 2720\nconsole.log(tournament.players[0].results); // [{ round: 1, color: 'w', opponentId: 4, result: '1' }, ...]\n\nconst trf = stringify(tournament); // back to TRF string\n```\n\n## Usage\n\n### `parse()`\n\n```typescript\nimport { parse } from '@echecs/trf';\n\nfunction parse(input: string, options?: ParseOptions): Tournament | null;\n```\n\nTakes a TRF string and returns a `Tournament` object, or `null` if the input\ncannot be parsed.\n\n- Strips BOM and surrounding whitespace automatically.\n- Never throws — parse failures call `options.onError` and return `null`.\n- Recoverable issues (unknown tags, malformed fields) call `options.onWarning`\n  and continue parsing.\n\n```typescript\nimport { parse } from '@echecs/trf';\n\nconst tournament = parse(trfString, {\n  onError: (err) =\u003e console.error(`Parse failed: ${err.message}`),\n  onWarning: (warn) =\u003e console.warn(`Warning: ${warn.message}`),\n});\n```\n\n### Result codes\n\nRound results on each player use the following codes:\n\n| Code | Meaning                                 |\n| ---- | --------------------------------------- |\n| `1`  | Win                                     |\n| `0`  | Loss                                    |\n| `=`  | Draw                                    |\n| `+`  | Forfeit win                             |\n| `-`  | Forfeit loss                            |\n| `D`  | Draw (unrated game, less than one move) |\n| `F`  | Full-point bye                          |\n| `H`  | Half-point bye                          |\n| `L`  | Loss (unrated game, less than one move) |\n| `U`  | Unplayed                                |\n| `W`  | Win (unrated game, less than one move)  |\n| `Z`  | Zero-point bye                          |\n\n### `stringify()`\n\n```typescript\nimport { stringify } from '@echecs/trf';\n\nfunction stringify(tournament: Tournament, options?: StringifyOptions): string;\n```\n\nTakes a `Tournament` object and returns a TRF string.\n\n- Never throws.\n- Omits optional header fields when absent.\n- `parse(stringify(t))` roundtrips cleanly for any valid `Tournament`.\n- Warns (via `options.onWarning`) when a player string field exceeds its column\n  width and will be truncated.\n\n```typescript\nimport { parse, stringify } from '@echecs/trf';\n\nconst t1 = parse(trfString)!;\n// ...modify t1...\nconst updated = stringify(t1, {\n  onWarning: (w) =\u003e console.warn(w.message),\n});\n```\n\n### Using with `@echecs/swiss`\n\n`@echecs/trf` has no dependency on `@echecs/swiss` by design. To use a parsed\ntournament as input to the Swiss pairing functions, adapt the types in your own\ncode:\n\n```typescript\nimport { parse } from '@echecs/trf';\nimport { pair } from '@echecs/swiss';\n\nimport type { Tournament } from '@echecs/trf';\nimport type { Game, Player } from '@echecs/swiss';\n\nfunction toPlayers(tournament: Tournament): Player[] {\n  return tournament.players.map((p) =\u003e ({\n    id: String(p.pairingNumber),\n    rating: p.rating,\n  }));\n}\n\nfunction toGames(tournament: Tournament): Game[][] {\n  const gamesByRound = new Map\u003cnumber, Game[]\u003e();\n  for (const player of tournament.players) {\n    for (const result of player.results) {\n      if (result.color !== 'w' || result.opponentId === null) continue;\n      let score: 0 | 0.5 | 1;\n      if (result.result === '1' || result.result === '+') score = 1;\n      else if (result.result === '0' || result.result === '-') score = 0;\n      else if (result.result === '=') score = 0.5;\n      else continue;\n      const games = gamesByRound.get(result.round) ?? [];\n      games.push({\n        black: String(result.opponentId),\n        result: score,\n        white: String(player.pairingNumber),\n      });\n      gamesByRound.set(result.round, games);\n    }\n  }\n  const roundCount = Math.max(0, ...gamesByRound.keys());\n  return Array.from(\n    { length: roundCount },\n    (_, i) =\u003e gamesByRound.get(i + 1) ?? [],\n  );\n}\n\nconst tournament = parse(trfString)!;\nconst pairings = pair(toPlayers(tournament), toGames(tournament));\n```\n\n## Types\n\n### `Tournament`\n\n```typescript\ninterface Tournament {\n  abnormalPoints?: AbnormalPoints[]; // Tag 299 — abnormal result overrides per round\n  absentPlayers?: number[]; // XXZ — pairing numbers absent for current round\n  acceleratedRounds?: AcceleratedRound[]; // Tag 250 — per-player fictitious points per round range\n  byes?: Bye[]; // Tag 240 — bye assignments per round\n  chiefArbiter?: string; // Tag 102\n  city?: string;\n  colourSequence?: string; // Tag 352 — e.g. 'WBWBWB'\n  comments?: string[]; // TRF26 '###' comment lines\n  deputyArbiters?: string[]; // Tag 112 — one entry per deputy arbiter line\n  encodedTimeControl?: string; // Tag 222 — e.g. '5400+30'\n  encodedTournamentType?: string; // Tag 192 — e.g. 'FIDE_DUTCH_2025'\n  endDate?: string;\n  federation?: string;\n  forfeitedMatches?: ForfeitedMatch[]; // Tag 330 — forfeited team matches per round\n  initialColour?: 'B' | 'W'; // Tag 152 / XXC white1/black1\n  name?: string;\n  numberOfPlayers?: number; // Tag 062\n  numberOfRatedPlayers?: number; // Tag 072\n  numberOfTeams?: number; // Tag 082\n  outOfOrderLineups?: OutOfOrderLineup[]; // Tag 300 — out-of-order team lineups per round\n  pairingController?: string; // Tag 092\n  playerAccelerations?: PlayerAcceleration[]; // XXA — per-player acceleration points\n  players: Player[];\n  prohibitedPairings?: ProhibitedPairing[]; // Tag 260 / XXP — forbidden pairings\n  roundDates?: string[]; // Tag 132 — one ISO date per round\n  rounds: number; // XXR — total planned round count\n  scoringSystem?: ScoringSystem; // Tag 162 / XXS\n  standingsTiebreaks?: string[]; // Tag 212 — codes for defining standings\n  startDate?: string;\n  startingRankMethod?: string; // Tag 172 — e.g. 'FRA FIDON'\n  teamPairingAllocatedByes?: TeamPairingAllocatedBye; // Tag 320\n  teamRoundResults?: TeamRoundResult[]; // Tags 801/802 — team round-by-round results\n  teams?: Team[]; // Tag 310\n  teamScoringSystem?: string; // Tag 362 — e.g. 'TW 2.0    TD 1.0    TL 0.0'\n  tiebreaks?: string[]; // Tag 202 — codes for breaking ties\n  timeControl?: string; // Tag 122\n  tournamentType?: string; // Tag 092 (TRF26) — free-form tournament type\n  useRankingId?: boolean; // XXC rank\n  version: Version; // 'TRF16' | 'TRF26'\n}\n```\n\n### `Player`\n\n```typescript\ninterface Player {\n  birthDate?: string;\n  federation?: string;\n  fideId?: string;\n  name: string;\n  nationalRatings?: NationalRating[]; // NRS records for this player\n  pairingNumber: number;\n  points: number;\n  rank: number;\n  rating?: number;\n  results: RoundResult[];\n  sex?: Sex; // 'm' | 'w'\n  title?: Title; // 'GM' | 'IM' | 'FM' | ...\n}\n```\n\n### `RoundResult`\n\n```typescript\ninterface RoundResult {\n  color: 'b' | 'w' | '-'; // '-' = no color assigned (bye/unplayed)\n  opponentId: number | null; // null for byes\n  result: ResultCode;\n  round: number;\n}\n```\n\n### `PlayerAcceleration`\n\nPer-player acceleration record (XXA). Stores fictitious extra points per round.\n\n```typescript\ninterface PlayerAcceleration {\n  pairingNumber: number;\n  points: number[]; // one value per round, indexed from 0\n}\n```\n\n### `ScoringSystem`\n\nCustom scoring weights for result types (Tag 162 / XXS).\n\n```typescript\ninterface ScoringSystem {\n  absence?: number;\n  blackDraw?: number;\n  blackLoss?: number;\n  blackWin?: number;\n  draw?: number;\n  forfeitLoss?: number;\n  forfeitWin?: number;\n  fullPointBye?: number;\n  halfPointBye?: number;\n  loss?: number;\n  pairingAllocatedBye?: number;\n  unknown?: number;\n  whiteDraw?: number;\n  whiteLoss?: number;\n  whiteWin?: number;\n  win?: number;\n  zeroPointBye?: number;\n}\n```\n\n### `TeamRoundResult`\n\nTeam round-by-round result record (tags `801` and `802`). One entry per line in\nthe TRF file. Tag `802` uses structured per-round fields; tag `801` stores raw\nper-round strings.\n\n```typescript\ninterface TeamRoundResult {\n  gamePoints: number;\n  matchPoints: number;\n  nickname?: string;\n  results: TeamRoundResult801[] | TeamRoundResult802[];\n  tag: '801' | '802';\n  teamId: number;\n}\n\ninterface TeamRoundResult801 {\n  opponentId: number | null;\n  raw: string; // e.g. 'b =0=1 1234'\n  round: number;\n  type?: 'FPB' | 'HPB' | 'PAB' | 'ZPB';\n}\n\ninterface TeamRoundResult802 {\n  color?: 'b' | 'w';\n  forfeit?: boolean;\n  gamePoints: number;\n  opponentId: number | null;\n  round: number;\n  type?: 'FPB' | 'HPB' | 'PAB' | 'ZPB';\n}\n```\n\n### `ParseOptions`\n\n```typescript\ninterface ParseOptions {\n  onError?: (error: ParseError) =\u003e void;\n  onWarning?: (warning: ParseWarning) =\u003e void;\n}\n```\n\n### `ParseError`\n\nReported via `ParseOptions.onError` when parsing fails unrecoverably. When an\nerror is reported, `parse()` returns `null`.\n\n```typescript\ninterface ParseError {\n  column: number; // 1-based column in the source\n  line: number; // 1-based line in the source\n  message: string;\n  offset: number; // byte offset in the source\n}\n```\n\n### `ParseWarning`\n\nReported via `ParseOptions.onWarning` (or `StringifyOptions.onWarning`) for\nrecoverable issues. Parsing continues after a warning.\n\n```typescript\ninterface ParseWarning {\n  column: number; // 1-based column in the source\n  line: number; // 1-based line in the source (player index for stringify)\n  message: string;\n  offset: number; // byte offset in the source (0 for stringify)\n}\n```\n\n### `StringifyOptions`\n\n```typescript\ninterface StringifyOptions {\n  onWarning?: (warning: ParseWarning) =\u003e void;\n}\n```\n\n### `ResultCode`\n\n```typescript\ntype ResultCode =\n  | '+' // forfeit win\n  | '-' // forfeit loss\n  | '0' // loss\n  | '1' // win\n  | '=' // draw\n  | 'D' // draw (unrated game, less than one move)\n  | 'F' // full-point bye\n  | 'H' // half-point bye\n  | 'L' // loss (unrated game, less than one move)\n  | 'U' // unplayed\n  | 'W' // win (unrated game, less than one move)\n  | 'Z'; // zero-point bye\n```\n\n### `Sex`\n\n```typescript\ntype Sex = 'm' | 'w';\n```\n\n### `Title`\n\nFIDE title codes.\n\n```typescript\ntype Title = 'CM' | 'FM' | 'GM' | 'IM' | 'WCM' | 'WFM' | 'WGM' | 'WIM';\n```\n\n### `Version`\n\n```typescript\ntype Version = 'TRF16' | 'TRF26';\n```\n\n## Supported Formats\n\n| Format | Status | Description                                                               |\n| ------ | ------ | ------------------------------------------------------------------------- |\n| TRF16  | Full   | FIDE TRF standard (2016)                                                  |\n| TRF26  | Full   | FIDE TRF standard (2026), all tags including 162, 192, 172, 222, 352, 362 |\n| TRFx   | Full   | JaVaFo extensions (XXC, XXZ, XXP, XXA, XXS)                               |\n\n## TRF Format Reference\n\nThe Tournament Report File (TRF) format is defined in the\n[FIDE Handbook](https://handbook.fide.com/files/handbook/TRF26.pdf). TRF16 is\nthe 2016 standard; TRF26 was approved by FIDE Council on 12/05/2025 and applied\nfrom 01/09/2025. TRFx is the de facto extension format used by JaVaFo, the FIDE\nreference pairing engine.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fechecsjs%2Ftrf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fechecsjs%2Ftrf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fechecsjs%2Ftrf/lists"}