fitzroy 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -106,13 +106,21 @@ var init_result = __esm({
106
106
  });
107
107
 
108
108
  // src/lib/date-utils.ts
109
- function parseFootyWireDate(dateStr, defaultYear) {
110
- const trimmed = dateStr.trim();
111
- if (trimmed === "") {
112
- return null;
109
+ function parseDate(raw, defaultYear) {
110
+ if (typeof raw === "number") {
111
+ const date = new Date(raw * 1e3);
112
+ return Number.isNaN(date.getTime()) ? null : date;
113
+ }
114
+ const trimmed = raw.trim();
115
+ if (trimmed === "") return null;
116
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) {
117
+ const stripped = trimmed.replace(/[Zz]$|[+-]\d{2}:\d{2}$/, "");
118
+ const utc = stripped.includes("T") ? `${stripped}Z` : `${stripped}T00:00:00Z`;
119
+ const date = new Date(utc);
120
+ return Number.isNaN(date.getTime()) ? null : date;
113
121
  }
114
122
  const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
115
- const normalised = withoutDow.replace(/-/g, " ");
123
+ const normalised = withoutDow.replace(/[-/]/g, " ");
116
124
  const fullMatch = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
117
125
  if (fullMatch) {
118
126
  const [, dayStr, monthStr, yearStr] = fullMatch;
@@ -120,6 +128,13 @@ function parseFootyWireDate(dateStr, defaultYear) {
120
128
  return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
121
129
  }
122
130
  }
131
+ const mdyMatch = /^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})$/.exec(normalised);
132
+ if (mdyMatch) {
133
+ const [, monthStr, dayStr, yearStr] = mdyMatch;
134
+ if (dayStr && monthStr && yearStr) {
135
+ return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
136
+ }
137
+ }
123
138
  const shortMatch = /^(\d{1,2})\s+([A-Za-z]+)(?:\s+(\d{1,2}):(\d{2})(am|pm))?$/i.exec(normalised);
124
139
  if (shortMatch && defaultYear != null) {
125
140
  const [, dayStr, monthStr, hourStr, minStr, ampm] = shortMatch;
@@ -127,40 +142,15 @@ function parseFootyWireDate(dateStr, defaultYear) {
127
142
  const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
128
143
  if (monthIndex === void 0) return null;
129
144
  const day = Number.parseInt(dayStr, 10);
130
- const hasTime = hourStr && minStr && ampm;
131
- if (!hasTime) {
145
+ if (!hourStr || !minStr || !ampm) {
132
146
  return buildUtcDate(defaultYear, monthStr, day);
133
147
  }
134
- let aestHours = Number.parseInt(hourStr, 10);
148
+ let hours = Number.parseInt(hourStr, 10);
135
149
  const minutes = Number.parseInt(minStr, 10);
136
- if (ampm.toLowerCase() === "pm" && aestHours < 12) aestHours += 12;
137
- if (ampm.toLowerCase() === "am" && aestHours === 12) aestHours = 0;
138
- const date = new Date(Date.UTC(defaultYear, monthIndex, day, aestHours - 10, minutes));
139
- if (Number.isNaN(date.getTime())) return null;
140
- return date;
141
- }
142
- return null;
143
- }
144
- function parseAflTablesDate(dateStr) {
145
- const trimmed = dateStr.trim();
146
- if (trimmed === "") {
147
- return null;
148
- }
149
- const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
150
- const normalised = withoutDow.replace(/[-/]/g, " ");
151
- const dmy = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
152
- if (dmy) {
153
- const [, dayStr, monthStr, yearStr] = dmy;
154
- if (dayStr && monthStr && yearStr) {
155
- return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
156
- }
157
- }
158
- const mdy = /^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})$/.exec(normalised);
159
- if (mdy) {
160
- const [, monthStr, dayStr, yearStr] = mdy;
161
- if (dayStr && monthStr && yearStr) {
162
- return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
163
- }
150
+ if (ampm.toLowerCase() === "pm" && hours < 12) hours += 12;
151
+ if (ampm.toLowerCase() === "am" && hours === 12) hours = 0;
152
+ const date = melbourneLocalToUtc(defaultYear, monthIndex, day, hours, minutes);
153
+ return Number.isNaN(date.getTime()) ? null : date;
164
154
  }
165
155
  return null;
166
156
  }
@@ -168,6 +158,20 @@ function resolveDefaultSeason(competition = "AFLM") {
168
158
  const year = (/* @__PURE__ */ new Date()).getFullYear();
169
159
  return competition === "AFLW" ? year - 1 : year;
170
160
  }
161
+ function melbourneLocalToUtc(year, monthIndex, day, hours, minutes) {
162
+ const aestGuess = new Date(Date.UTC(year, monthIndex, day, hours - 10, minutes));
163
+ const parts = new Intl.DateTimeFormat("en-AU", {
164
+ timeZone: "Australia/Melbourne",
165
+ day: "2-digit",
166
+ hour: "2-digit",
167
+ hour12: false
168
+ }).formatToParts(aestGuess);
169
+ const getNum = (type) => Number(parts.find((p) => p.type === type)?.value);
170
+ if (getNum("day") === day && getNum("hour") === hours % 24) {
171
+ return aestGuess;
172
+ }
173
+ return new Date(Date.UTC(year, monthIndex, day, hours - 11, minutes));
174
+ }
171
175
  function buildUtcDate(year, monthStr, day) {
172
176
  const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
173
177
  if (monthIndex === void 0) {
@@ -702,7 +706,7 @@ function transformMatchItems(items, season, competition, source = "afl-api") {
702
706
  roundNumber: item.round?.roundNumber ?? 0,
703
707
  roundType: inferRoundType(item.round?.name ?? ""),
704
708
  roundName: item.round?.name ?? null,
705
- date: new Date(item.match.utcStartTime),
709
+ date: parseDate(item.match.utcStartTime) ?? new Date(item.match.utcStartTime),
706
710
  venue: item.venue?.name ? normaliseVenueName(item.venue.name) : "",
707
711
  homeTeam: normaliseTeamName(item.match.homeTeam.name),
708
712
  awayTeam: normaliseTeamName(item.match.awayTeam.name),
@@ -741,6 +745,7 @@ var FINALS_PATTERN, ROUND_CODE_MAP, ROUND_NUMBER_PATTERN;
741
745
  var init_match_results = __esm({
742
746
  "src/transforms/match-results.ts"() {
743
747
  "use strict";
748
+ init_date_utils();
744
749
  init_team_mapping();
745
750
  init_venue_mapping();
746
751
  FINALS_PATTERN = /final|elimination|qualifying|preliminary|semi|grand/i;
@@ -801,7 +806,7 @@ function parseMatchList(html, year) {
801
806
  const scoreLink = scoreCell.find("a").attr("href") ?? "";
802
807
  const midMatch = /mid=(\d+)/.exec(scoreLink);
803
808
  const matchId = midMatch?.[1] ? `FW_${midMatch[1]}` : `FW_${year}_R${currentRound}_${homeTeam}`;
804
- const date = parseFootyWireDate(dateText, year) ?? new Date(Date.UTC(year, 0, 1));
809
+ const date = parseDate(dateText, year) ?? new Date(Date.UTC(year, 0, 1));
805
810
  const homeGoals = Math.floor(homePoints / 6);
806
811
  const homeBehinds = homePoints - homeGoals * 6;
807
812
  const awayGoals = Math.floor(awayPoints / 6);
@@ -881,7 +886,7 @@ function parseFixtureList(html, year) {
881
886
  if (teamLinks.length < 2) return;
882
887
  const homeTeam = normaliseTeamName($(teamLinks[0]).text().trim());
883
888
  const awayTeam = normaliseTeamName($(teamLinks[1]).text().trim());
884
- const date = parseFootyWireDate(dateText, year) ?? new Date(Date.UTC(year, 0, 1));
889
+ const date = parseDate(dateText, year) ?? new Date(Date.UTC(year, 0, 1));
885
890
  gameNumber++;
886
891
  const scoreCell = cells.length >= 5 ? $(cells[4]) : null;
887
892
  const scoreText = scoreCell?.text().trim() ?? "";
@@ -971,7 +976,7 @@ function teamNameToFootyWireSlug(teamName) {
971
976
  }
972
977
  function normaliseDob(raw) {
973
978
  if (!raw) return null;
974
- const parsed = parseFootyWireDate(raw);
979
+ const parsed = parseDate(raw);
975
980
  if (parsed) return parsed.toISOString().slice(0, 10);
976
981
  return raw;
977
982
  }
@@ -2365,7 +2370,7 @@ function transformSquiggleGamesToResults(games, season) {
2365
2370
  roundNumber: g.round,
2366
2371
  roundType: inferRoundType(g.roundname),
2367
2372
  roundName: g.roundname || null,
2368
- date: new Date(g.unixtime * 1e3),
2373
+ date: parseDate(g.unixtime) ?? new Date(g.unixtime * 1e3),
2369
2374
  venue: normaliseVenueName(g.venue),
2370
2375
  homeTeam: normaliseTeamName(g.hteam),
2371
2376
  awayTeam: normaliseTeamName(g.ateam),
@@ -2405,7 +2410,7 @@ function transformSquiggleGamesToFixture(games, season) {
2405
2410
  season,
2406
2411
  roundNumber: g.round,
2407
2412
  roundType: inferRoundType(g.roundname),
2408
- date: new Date(g.unixtime * 1e3),
2413
+ date: parseDate(g.unixtime) ?? new Date(g.unixtime * 1e3),
2409
2414
  venue: normaliseVenueName(g.venue),
2410
2415
  homeTeam: normaliseTeamName(g.hteam),
2411
2416
  awayTeam: normaliseTeamName(g.ateam),
@@ -2431,6 +2436,7 @@ function transformSquiggleStandings(standings) {
2431
2436
  var init_squiggle2 = __esm({
2432
2437
  "src/transforms/squiggle.ts"() {
2433
2438
  "use strict";
2439
+ init_date_utils();
2434
2440
  init_team_mapping();
2435
2441
  init_venue_mapping();
2436
2442
  init_match_results();
@@ -2444,7 +2450,7 @@ function toFixture(item, season, fallbackRoundNumber, competition) {
2444
2450
  season,
2445
2451
  roundNumber: item.round?.roundNumber ?? fallbackRoundNumber,
2446
2452
  roundType: inferRoundType(item.round?.name ?? ""),
2447
- date: new Date(item.match.utcStartTime),
2453
+ date: parseDate(item.match.utcStartTime) ?? new Date(item.match.utcStartTime),
2448
2454
  venue: normaliseVenueName(item.venue?.name ?? ""),
2449
2455
  homeTeam: normaliseTeamName(item.match.homeTeam.name),
2450
2456
  awayTeam: normaliseTeamName(item.match.awayTeam.name),
@@ -2511,6 +2517,7 @@ var init_fixture = __esm({
2511
2517
  "src/api/fixture.ts"() {
2512
2518
  "use strict";
2513
2519
  init_concurrency();
2520
+ init_date_utils();
2514
2521
  init_errors();
2515
2522
  init_result();
2516
2523
  init_team_mapping();
@@ -2762,9 +2769,9 @@ function parseQuarterScores(text) {
2762
2769
  function parseDateFromInfo(text, year) {
2763
2770
  const dateMatch = /(\d{1,2}-[A-Z][a-z]{2}-\d{4})/.exec(text);
2764
2771
  if (dateMatch?.[1]) {
2765
- return parseAflTablesDate(dateMatch[1]) ?? new Date(year, 0, 1);
2772
+ return parseDate(dateMatch[1]) ?? new Date(year, 0, 1);
2766
2773
  }
2767
- return parseAflTablesDate(text) ?? new Date(year, 0, 1);
2774
+ return parseDate(text) ?? new Date(year, 0, 1);
2768
2775
  }
2769
2776
  function parseVenueFromInfo(html) {
2770
2777
  const $ = cheerio5.load(html);
@@ -3791,7 +3798,7 @@ function mapRow(i, c, isAflw, competition) {
3791
3798
  roundNumber: roundAt(c.round, i),
3792
3799
  team: normaliseTeamName(team),
3793
3800
  competition,
3794
- date: dateStr ? new Date(dateStr) : null,
3801
+ date: dateStr ? parseDate(dateStr) : null,
3795
3802
  homeTeam: homeTeam ? normaliseTeamName(homeTeam) : null,
3796
3803
  awayTeam: awayTeam ? normaliseTeamName(awayTeam) : null,
3797
3804
  playerId: String(c.playerId?.[i] ?? ""),
@@ -3873,6 +3880,7 @@ var REQUIRED_COLUMN_GROUPS, AFLM_COLUMNS, AFLW_COLUMNS;
3873
3880
  var init_fryzigg_player_stats = __esm({
3874
3881
  "src/transforms/fryzigg-player-stats.ts"() {
3875
3882
  "use strict";
3883
+ init_date_utils();
3876
3884
  init_errors();
3877
3885
  init_result();
3878
3886
  init_team_mapping();
@@ -4094,7 +4102,7 @@ async function fetchPlayerStats(query) {
4094
4102
  competition,
4095
4103
  source: "afl-api",
4096
4104
  teamIdMap,
4097
- date: new Date(item.match.utcStartTime),
4105
+ date: parseDate(item.match.utcStartTime) ?? new Date(item.match.utcStartTime),
4098
4106
  homeTeam: normaliseTeamName(item.match.homeTeam.name),
4099
4107
  awayTeam: normaliseTeamName(item.match.awayTeam.name)
4100
4108
  })
@@ -4157,6 +4165,7 @@ var init_player_stats2 = __esm({
4157
4165
  "src/api/player-stats.ts"() {
4158
4166
  "use strict";
4159
4167
  init_concurrency();
4168
+ init_date_utils();
4160
4169
  init_errors();
4161
4170
  init_result();
4162
4171
  init_team_mapping();
@@ -4283,7 +4292,7 @@ async function fetchSquad(query) {
4283
4292
  displayName: `${p.player.firstName} ${p.player.surname}`,
4284
4293
  jumperNumber: p.jumperNumber ?? null,
4285
4294
  position: p.position ?? null,
4286
- dateOfBirth: p.player.dateOfBirth ? new Date(p.player.dateOfBirth) : null,
4295
+ dateOfBirth: p.player.dateOfBirth ? parseDate(p.player.dateOfBirth) : null,
4287
4296
  heightCm: p.player.heightInCm || null,
4288
4297
  weightKg: p.player.weightInKg || null,
4289
4298
  draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
@@ -4303,6 +4312,7 @@ async function fetchSquad(query) {
4303
4312
  var init_teams = __esm({
4304
4313
  "src/api/teams.ts"() {
4305
4314
  "use strict";
4315
+ init_date_utils();
4306
4316
  init_errors();
4307
4317
  init_result();
4308
4318
  init_team_mapping();
@@ -5786,7 +5796,7 @@ resolveAliases();
5786
5796
  var main = defineCommand11({
5787
5797
  meta: {
5788
5798
  name: "fitzroy",
5789
- version: "1.7.0",
5799
+ version: "1.7.2",
5790
5800
  description: "TypeScript port of the fitzRoy R package \u2014 fetch AFL data from the command line"
5791
5801
  },
5792
5802
  subCommands: {
package/dist/index.d.ts CHANGED
@@ -598,65 +598,32 @@ declare function fetchSquad2(query: SquadQuery): Promise<Result<Squad, Error>>;
598
598
  * @module
599
599
  */
600
600
  /**
601
- * Parse a UTC ISO 8601 string from the AFL API into a Date.
601
+ * Parse any AFL date string or timestamp into a correct UTC Date.
602
602
  *
603
- * The AFL API returns dates like `"2024-03-14T06:20:00.000Z"` or
604
- * `"2024-03-14T06:20:00Z"`.
603
+ * Accepts every format seen across AFL data sources and always returns
604
+ * a proper UTC Date. Input format is auto-detected:
605
605
  *
606
- * @param iso - A UTC ISO 8601 date string
607
- * @returns A Date object, or null if parsing fails
606
+ * | Input | Source | Handling |
607
+ * |---|---|---|
608
+ * | `1709622000` (number) | Squiggle unix timestamp | × 1000 → UTC |
609
+ * | `"2026-03-05T08:30:00"` | AFL API `utcStartTime` (no Z) | Force UTC |
610
+ * | `"2026-03-05T08:30:00.000Z"` | AFL API (with Z) | Parse as UTC |
611
+ * | `"Thu 13 Mar 7:30pm"` | FootyWire (Melbourne local) | AEST/AEDT → UTC |
612
+ * | `"16-Mar-2024"` / `"16 Mar 2024"` | AFL Tables / FootyWire | Midnight UTC |
613
+ * | `"Sat 16 Mar 2024"` | FootyWire (day-of-week prefix) | Midnight UTC |
608
614
  *
609
- * @example
610
- * ```ts
611
- * parseAflApiDate("2024-03-14T06:20:00.000Z")
612
- * // => Date(2024-03-14T06:20:00.000Z)
613
- * ```
615
+ * @param raw - A date string or unix timestamp (seconds)
616
+ * @param defaultYear - Year to use when the string lacks one (FootyWire fixtures)
617
+ * @returns A Date object in UTC, or null if parsing fails
614
618
  */
619
+ declare function parseDate(raw: string | number, defaultYear?: number): Date | null;
620
+ /** @deprecated Use {@link parseDate} instead. */
615
621
  declare function parseAflApiDate(iso: string): Date | null;
616
- /**
617
- * Parse a date string from FootyWire into a Date.
618
- *
619
- * FootyWire uses formats like:
620
- * - `"Sat 16 Mar 2024"` (day-of-week, day, month-abbrev, year)
621
- * - `"16 Mar 2024"` (day, month-abbrev, year)
622
- * - `"16-Mar-2024"` (day-month-year with hyphens)
623
- * - `"Thu 13 Mar 7:30pm"` (day-of-week, day, month, time — no year)
624
- * - `"13 Mar"` (day, month — no year)
625
- *
626
- * @param dateStr - A FootyWire date string
627
- * @param defaultYear - Year to use when the string lacks one (e.g. fixture pages)
628
- * @returns A Date object (UTC), or null if parsing fails
629
- *
630
- * @example
631
- * ```ts
632
- * parseFootyWireDate("Sat 16 Mar 2024")
633
- * // => Date(2024-03-16T00:00:00.000Z)
634
- *
635
- * parseFootyWireDate("Thu 13 Mar 7:30pm", 2025)
636
- * // => Date(2025-03-13T09:30:00.000Z) — time stored as AEST offset from UTC
637
- * ```
638
- */
622
+ /** @deprecated Use {@link parseDate} instead. */
623
+ declare function parseAflApiMatchTime(iso: string): Date | null;
624
+ /** @deprecated Use {@link parseDate} instead. */
639
625
  declare function parseFootyWireDate(dateStr: string, defaultYear?: number): Date | null;
640
- /**
641
- * Parse a date string from AFL Tables into a Date.
642
- *
643
- * AFL Tables uses formats like:
644
- * - `"16-Mar-2024"` (DD-Mon-YYYY)
645
- * - `"Sat 16-Mar-2024"` (Dow DD-Mon-YYYY)
646
- * - `"16 Mar 2024"` (DD Mon YYYY)
647
- *
648
- * For very old historical matches, dates may be partial (e.g. just a year),
649
- * which are not supported and return null.
650
- *
651
- * @param dateStr - An AFL Tables date string
652
- * @returns A Date object (midnight UTC), or null if parsing fails
653
- *
654
- * @example
655
- * ```ts
656
- * parseAflTablesDate("16-Mar-2024")
657
- * // => Date(2024-03-16T00:00:00.000Z)
658
- * ```
659
- */
626
+ /** @deprecated Use {@link parseDate} instead. */
660
627
  declare function parseAflTablesDate(dateStr: string): Date | null;
661
628
  /**
662
629
  * Format a Date as an AEST/AEDT-aware display string.
@@ -666,12 +633,6 @@ declare function parseAflTablesDate(dateStr: string): Date | null;
666
633
  *
667
634
  * @param date - The Date to format
668
635
  * @returns A formatted string like `"Thu 14 Mar 2024 5:20 PM AEDT"`
669
- *
670
- * @example
671
- * ```ts
672
- * toAestString(new Date("2024-03-14T06:20:00.000Z"))
673
- * // => "Thu 14 Mar 2024 5:20 PM AEDT"
674
- * ```
675
636
  */
676
637
  declare function toAestString(date: Date): string;
677
638
  /**
@@ -2299,4 +2260,4 @@ declare function transformSquiggleGamesToFixture(games: readonly SquiggleGame[],
2299
2260
  * Transform Squiggle standings into LadderEntry objects.
2300
2261
  */
2301
2262
  declare function transformSquiggleStandings(standings: readonly SquiggleStanding[]): LadderEntry[];
2302
- export { transformSquiggleStandings, transformSquiggleGamesToResults, transformSquiggleGamesToFixture, transformPlayerStats, transformMatchRoster, transformMatchItems, transformLadderEntries, transformFryziggPlayerStats, toAestString, parseFootyWireDate, parseAflTablesDate, parseAflApiDate, ok, normaliseVenueName, normaliseTeamName, inferRoundType, fetchTeams2 as fetchTeams, fetchTeamStats2 as fetchTeamStats, fetchSquad2 as fetchSquad, fetchPlayerStats2 as fetchPlayerStats, fetchPlayerDetails, fetchMatchResults, fetchLineup, fetchLadder2 as fetchLadder, fetchFixture, fetchCoachesVotes, fetchAwards, err, computeLadder, ValidationError, UnsupportedSourceError, TransformContext, TeamStatsSummaryType, TeamStatsQuery, TeamStatsEntry, TeamScoreSchema, TeamScore, TeamQuery, TeamPlayersSchema, TeamPlayers, TeamListSchema, TeamList, TeamItemSchema, TeamItem, Team, SquiggleStandingsResponseSchema, SquiggleStandingsResponse, SquiggleStandingSchema, SquiggleStanding, SquiggleGamesResponseSchema, SquiggleGamesResponse, SquiggleGameSchema, SquiggleGame, SquiggleClientOptions, SquiggleClient, SquadSchema, SquadQuery, SquadPlayerItemSchema, SquadPlayerItem, SquadPlayerInnerSchema, SquadPlayer, SquadListSchema, SquadList, Squad, SeasonRoundQuery, ScrapeError, ScoreSchema, Score, RoundType, RoundSchema, RoundListSchema, RoundList, Round, RosterPlayerSchema, RosterPlayer, RisingStarNomination, Result, QuarterScore, PlayerStatsQuery, PlayerStatsListSchema, PlayerStatsList, PlayerStatsItemSchema, PlayerStatsItem, PlayerStats, PlayerGameStatsSchema, PlayerGameStats, PlayerDetailsQuery, PlayerDetails, PeriodScoreSchema, PeriodScore, Ok, MatchStatus, MatchRosterSchema, MatchRoster, MatchResult, MatchQuery, MatchItemSchema, MatchItemListSchema, MatchItemList, MatchItem, LineupQuery, LineupPlayer, Lineup, LadderResponseSchema, LadderResponse, LadderQuery, LadderEntryRawSchema, LadderEntryRaw, LadderEntry, Ladder, FryziggTransformOptions, FryziggClientOptions, FryziggClient, FootyWireClientOptions, FootyWireClient, Fixture, Err, DataSource, CompseasonSchema, CompseasonListSchema, CompseasonList, Compseason, CompetitionSchema, CompetitionListSchema, CompetitionList, CompetitionCode, Competition, CoachesVoteQuery, CoachesVote, CfsVenueSchema, CfsVenue, CfsScoreSchema, CfsScore, CfsMatchTeamSchema, CfsMatchTeam, CfsMatchSchema, CfsMatch, BrownlowVote, AwardType, AwardQuery, Award, AllAustralianSelection, AflTablesClientOptions, AflTablesClient, AflCoachesClientOptions, AflCoachesClient, AflApiTokenSchema, AflApiToken, AflApiError, AflApiClientOptions, AflApiClient };
2263
+ export { transformSquiggleStandings, transformSquiggleGamesToResults, transformSquiggleGamesToFixture, transformPlayerStats, transformMatchRoster, transformMatchItems, transformLadderEntries, transformFryziggPlayerStats, toAestString, parseFootyWireDate, parseDate, parseAflTablesDate, parseAflApiMatchTime, parseAflApiDate, ok, normaliseVenueName, normaliseTeamName, inferRoundType, fetchTeams2 as fetchTeams, fetchTeamStats2 as fetchTeamStats, fetchSquad2 as fetchSquad, fetchPlayerStats2 as fetchPlayerStats, fetchPlayerDetails, fetchMatchResults, fetchLineup, fetchLadder2 as fetchLadder, fetchFixture, fetchCoachesVotes, fetchAwards, err, computeLadder, ValidationError, UnsupportedSourceError, TransformContext, TeamStatsSummaryType, TeamStatsQuery, TeamStatsEntry, TeamScoreSchema, TeamScore, TeamQuery, TeamPlayersSchema, TeamPlayers, TeamListSchema, TeamList, TeamItemSchema, TeamItem, Team, SquiggleStandingsResponseSchema, SquiggleStandingsResponse, SquiggleStandingSchema, SquiggleStanding, SquiggleGamesResponseSchema, SquiggleGamesResponse, SquiggleGameSchema, SquiggleGame, SquiggleClientOptions, SquiggleClient, SquadSchema, SquadQuery, SquadPlayerItemSchema, SquadPlayerItem, SquadPlayerInnerSchema, SquadPlayer, SquadListSchema, SquadList, Squad, SeasonRoundQuery, ScrapeError, ScoreSchema, Score, RoundType, RoundSchema, RoundListSchema, RoundList, Round, RosterPlayerSchema, RosterPlayer, RisingStarNomination, Result, QuarterScore, PlayerStatsQuery, PlayerStatsListSchema, PlayerStatsList, PlayerStatsItemSchema, PlayerStatsItem, PlayerStats, PlayerGameStatsSchema, PlayerGameStats, PlayerDetailsQuery, PlayerDetails, PeriodScoreSchema, PeriodScore, Ok, MatchStatus, MatchRosterSchema, MatchRoster, MatchResult, MatchQuery, MatchItemSchema, MatchItemListSchema, MatchItemList, MatchItem, LineupQuery, LineupPlayer, Lineup, LadderResponseSchema, LadderResponse, LadderQuery, LadderEntryRawSchema, LadderEntryRaw, LadderEntry, Ladder, FryziggTransformOptions, FryziggClientOptions, FryziggClient, FootyWireClientOptions, FootyWireClient, Fixture, Err, DataSource, CompseasonSchema, CompseasonListSchema, CompseasonList, Compseason, CompetitionSchema, CompetitionListSchema, CompetitionList, CompetitionCode, Competition, CoachesVoteQuery, CoachesVote, CfsVenueSchema, CfsVenue, CfsScoreSchema, CfsScore, CfsMatchTeamSchema, CfsMatchTeam, CfsMatchSchema, CfsMatch, BrownlowVote, AwardType, AwardQuery, Award, AllAustralianSelection, AflTablesClientOptions, AflTablesClient, AflCoachesClientOptions, AflCoachesClient, AflApiTokenSchema, AflApiToken, AflApiError, AflApiClientOptions, AflApiClient };
package/dist/index.js CHANGED
@@ -46,20 +46,21 @@ function err(error) {
46
46
  import * as cheerio2 from "cheerio";
47
47
 
48
48
  // src/lib/date-utils.ts
49
- function parseAflApiDate(iso) {
50
- const date = new Date(iso);
51
- if (Number.isNaN(date.getTime())) {
52
- return null;
49
+ function parseDate(raw, defaultYear) {
50
+ if (typeof raw === "number") {
51
+ const date = new Date(raw * 1e3);
52
+ return Number.isNaN(date.getTime()) ? null : date;
53
53
  }
54
- return date;
55
- }
56
- function parseFootyWireDate(dateStr, defaultYear) {
57
- const trimmed = dateStr.trim();
58
- if (trimmed === "") {
59
- return null;
54
+ const trimmed = raw.trim();
55
+ if (trimmed === "") return null;
56
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) {
57
+ const stripped = trimmed.replace(/[Zz]$|[+-]\d{2}:\d{2}$/, "");
58
+ const utc = stripped.includes("T") ? `${stripped}Z` : `${stripped}T00:00:00Z`;
59
+ const date = new Date(utc);
60
+ return Number.isNaN(date.getTime()) ? null : date;
60
61
  }
61
62
  const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
62
- const normalised = withoutDow.replace(/-/g, " ");
63
+ const normalised = withoutDow.replace(/[-/]/g, " ");
63
64
  const fullMatch = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
64
65
  if (fullMatch) {
65
66
  const [, dayStr, monthStr, yearStr] = fullMatch;
@@ -67,6 +68,13 @@ function parseFootyWireDate(dateStr, defaultYear) {
67
68
  return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
68
69
  }
69
70
  }
71
+ const mdyMatch = /^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})$/.exec(normalised);
72
+ if (mdyMatch) {
73
+ const [, monthStr, dayStr, yearStr] = mdyMatch;
74
+ if (dayStr && monthStr && yearStr) {
75
+ return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
76
+ }
77
+ }
70
78
  const shortMatch = /^(\d{1,2})\s+([A-Za-z]+)(?:\s+(\d{1,2}):(\d{2})(am|pm))?$/i.exec(normalised);
71
79
  if (shortMatch && defaultYear != null) {
72
80
  const [, dayStr, monthStr, hourStr, minStr, ampm] = shortMatch;
@@ -74,42 +82,29 @@ function parseFootyWireDate(dateStr, defaultYear) {
74
82
  const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
75
83
  if (monthIndex === void 0) return null;
76
84
  const day = Number.parseInt(dayStr, 10);
77
- const hasTime = hourStr && minStr && ampm;
78
- if (!hasTime) {
85
+ if (!hourStr || !minStr || !ampm) {
79
86
  return buildUtcDate(defaultYear, monthStr, day);
80
87
  }
81
- let aestHours = Number.parseInt(hourStr, 10);
88
+ let hours = Number.parseInt(hourStr, 10);
82
89
  const minutes = Number.parseInt(minStr, 10);
83
- if (ampm.toLowerCase() === "pm" && aestHours < 12) aestHours += 12;
84
- if (ampm.toLowerCase() === "am" && aestHours === 12) aestHours = 0;
85
- const date = new Date(Date.UTC(defaultYear, monthIndex, day, aestHours - 10, minutes));
86
- if (Number.isNaN(date.getTime())) return null;
87
- return date;
90
+ if (ampm.toLowerCase() === "pm" && hours < 12) hours += 12;
91
+ if (ampm.toLowerCase() === "am" && hours === 12) hours = 0;
92
+ const date = melbourneLocalToUtc(defaultYear, monthIndex, day, hours, minutes);
93
+ return Number.isNaN(date.getTime()) ? null : date;
88
94
  }
89
95
  return null;
90
96
  }
97
+ function parseAflApiDate(iso) {
98
+ return parseDate(iso);
99
+ }
100
+ function parseAflApiMatchTime(iso) {
101
+ return parseDate(iso);
102
+ }
103
+ function parseFootyWireDate(dateStr, defaultYear) {
104
+ return parseDate(dateStr, defaultYear);
105
+ }
91
106
  function parseAflTablesDate(dateStr) {
92
- const trimmed = dateStr.trim();
93
- if (trimmed === "") {
94
- return null;
95
- }
96
- const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
97
- const normalised = withoutDow.replace(/[-/]/g, " ");
98
- const dmy = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
99
- if (dmy) {
100
- const [, dayStr, monthStr, yearStr] = dmy;
101
- if (dayStr && monthStr && yearStr) {
102
- return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
103
- }
104
- }
105
- const mdy = /^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})$/.exec(normalised);
106
- if (mdy) {
107
- const [, monthStr, dayStr, yearStr] = mdy;
108
- if (dayStr && monthStr && yearStr) {
109
- return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
110
- }
111
- }
112
- return null;
107
+ return parseDate(dateStr);
113
108
  }
114
109
  function toAestString(date) {
115
110
  const formatter = new Intl.DateTimeFormat("en-AU", {
@@ -154,6 +149,20 @@ var MONTH_ABBREV_TO_INDEX = /* @__PURE__ */ new Map([
154
149
  ["november", 10],
155
150
  ["december", 11]
156
151
  ]);
152
+ function melbourneLocalToUtc(year, monthIndex, day, hours, minutes) {
153
+ const aestGuess = new Date(Date.UTC(year, monthIndex, day, hours - 10, minutes));
154
+ const parts = new Intl.DateTimeFormat("en-AU", {
155
+ timeZone: "Australia/Melbourne",
156
+ day: "2-digit",
157
+ hour: "2-digit",
158
+ hour12: false
159
+ }).formatToParts(aestGuess);
160
+ const getNum = (type) => Number(parts.find((p) => p.type === type)?.value);
161
+ if (getNum("day") === day && getNum("hour") === hours % 24) {
162
+ return aestGuess;
163
+ }
164
+ return new Date(Date.UTC(year, monthIndex, day, hours - 11, minutes));
165
+ }
157
166
  function buildUtcDate(year, monthStr, day) {
158
167
  const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
159
168
  if (monthIndex === void 0) {
@@ -643,7 +652,7 @@ function transformMatchItems(items, season, competition, source = "afl-api") {
643
652
  roundNumber: item.round?.roundNumber ?? 0,
644
653
  roundType: inferRoundType(item.round?.name ?? ""),
645
654
  roundName: item.round?.name ?? null,
646
- date: new Date(item.match.utcStartTime),
655
+ date: parseDate(item.match.utcStartTime) ?? new Date(item.match.utcStartTime),
647
656
  venue: item.venue?.name ? normaliseVenueName(item.venue.name) : "",
648
657
  homeTeam: normaliseTeamName(item.match.homeTeam.name),
649
658
  awayTeam: normaliseTeamName(item.match.awayTeam.name),
@@ -958,7 +967,7 @@ function parseMatchList(html, year) {
958
967
  const scoreLink = scoreCell.find("a").attr("href") ?? "";
959
968
  const midMatch = /mid=(\d+)/.exec(scoreLink);
960
969
  const matchId = midMatch?.[1] ? `FW_${midMatch[1]}` : `FW_${year}_R${currentRound}_${homeTeam}`;
961
- const date = parseFootyWireDate(dateText, year) ?? new Date(Date.UTC(year, 0, 1));
970
+ const date = parseDate(dateText, year) ?? new Date(Date.UTC(year, 0, 1));
962
971
  const homeGoals = Math.floor(homePoints / 6);
963
972
  const homeBehinds = homePoints - homeGoals * 6;
964
973
  const awayGoals = Math.floor(awayPoints / 6);
@@ -1038,7 +1047,7 @@ function parseFixtureList(html, year) {
1038
1047
  if (teamLinks.length < 2) return;
1039
1048
  const homeTeam = normaliseTeamName($(teamLinks[0]).text().trim());
1040
1049
  const awayTeam = normaliseTeamName($(teamLinks[1]).text().trim());
1041
- const date = parseFootyWireDate(dateText, year) ?? new Date(Date.UTC(year, 0, 1));
1050
+ const date = parseDate(dateText, year) ?? new Date(Date.UTC(year, 0, 1));
1042
1051
  gameNumber++;
1043
1052
  const scoreCell = cells.length >= 5 ? $(cells[4]) : null;
1044
1053
  const scoreText = scoreCell?.text().trim() ?? "";
@@ -1148,7 +1157,7 @@ function teamNameToFootyWireSlug(teamName) {
1148
1157
  }
1149
1158
  function normaliseDob(raw) {
1150
1159
  if (!raw) return null;
1151
- const parsed = parseFootyWireDate(raw);
1160
+ const parsed = parseDate(raw);
1152
1161
  if (parsed) return parsed.toISOString().slice(0, 10);
1153
1162
  return raw;
1154
1163
  }
@@ -2385,7 +2394,7 @@ function transformSquiggleGamesToResults(games, season) {
2385
2394
  roundNumber: g.round,
2386
2395
  roundType: inferRoundType(g.roundname),
2387
2396
  roundName: g.roundname || null,
2388
- date: new Date(g.unixtime * 1e3),
2397
+ date: parseDate(g.unixtime) ?? new Date(g.unixtime * 1e3),
2389
2398
  venue: normaliseVenueName(g.venue),
2390
2399
  homeTeam: normaliseTeamName(g.hteam),
2391
2400
  awayTeam: normaliseTeamName(g.ateam),
@@ -2425,7 +2434,7 @@ function transformSquiggleGamesToFixture(games, season) {
2425
2434
  season,
2426
2435
  roundNumber: g.round,
2427
2436
  roundType: inferRoundType(g.roundname),
2428
- date: new Date(g.unixtime * 1e3),
2437
+ date: parseDate(g.unixtime) ?? new Date(g.unixtime * 1e3),
2429
2438
  venue: normaliseVenueName(g.venue),
2430
2439
  homeTeam: normaliseTeamName(g.hteam),
2431
2440
  awayTeam: normaliseTeamName(g.ateam),
@@ -2456,7 +2465,7 @@ function toFixture(item, season, fallbackRoundNumber, competition) {
2456
2465
  season,
2457
2466
  roundNumber: item.round?.roundNumber ?? fallbackRoundNumber,
2458
2467
  roundType: inferRoundType(item.round?.name ?? ""),
2459
- date: new Date(item.match.utcStartTime),
2468
+ date: parseDate(item.match.utcStartTime) ?? new Date(item.match.utcStartTime),
2460
2469
  venue: normaliseVenueName(item.venue?.name ?? ""),
2461
2470
  homeTeam: normaliseTeamName(item.match.homeTeam.name),
2462
2471
  awayTeam: normaliseTeamName(item.match.awayTeam.name),
@@ -2926,9 +2935,9 @@ function parseQuarterScores(text) {
2926
2935
  function parseDateFromInfo(text, year) {
2927
2936
  const dateMatch = /(\d{1,2}-[A-Z][a-z]{2}-\d{4})/.exec(text);
2928
2937
  if (dateMatch?.[1]) {
2929
- return parseAflTablesDate(dateMatch[1]) ?? new Date(year, 0, 1);
2938
+ return parseDate(dateMatch[1]) ?? new Date(year, 0, 1);
2930
2939
  }
2931
- return parseAflTablesDate(text) ?? new Date(year, 0, 1);
2940
+ return parseDate(text) ?? new Date(year, 0, 1);
2932
2941
  }
2933
2942
  function parseVenueFromInfo(html) {
2934
2943
  const $ = cheerio6.load(html);
@@ -3747,7 +3756,7 @@ function mapRow(i, c, isAflw, competition) {
3747
3756
  roundNumber: roundAt(c.round, i),
3748
3757
  team: normaliseTeamName(team),
3749
3758
  competition,
3750
- date: dateStr ? new Date(dateStr) : null,
3759
+ date: dateStr ? parseDate(dateStr) : null,
3751
3760
  homeTeam: homeTeam ? normaliseTeamName(homeTeam) : null,
3752
3761
  awayTeam: awayTeam ? normaliseTeamName(awayTeam) : null,
3753
3762
  playerId: String(c.playerId?.[i] ?? ""),
@@ -3983,7 +3992,7 @@ async function fetchPlayerStats(query) {
3983
3992
  competition,
3984
3993
  source: "afl-api",
3985
3994
  teamIdMap,
3986
- date: new Date(item.match.utcStartTime),
3995
+ date: parseDate(item.match.utcStartTime) ?? new Date(item.match.utcStartTime),
3987
3996
  homeTeam: normaliseTeamName(item.match.homeTeam.name),
3988
3997
  awayTeam: normaliseTeamName(item.match.awayTeam.name)
3989
3998
  })
@@ -4147,7 +4156,7 @@ async function fetchSquad(query) {
4147
4156
  displayName: `${p.player.firstName} ${p.player.surname}`,
4148
4157
  jumperNumber: p.jumperNumber ?? null,
4149
4158
  position: p.position ?? null,
4150
- dateOfBirth: p.player.dateOfBirth ? new Date(p.player.dateOfBirth) : null,
4159
+ dateOfBirth: p.player.dateOfBirth ? parseDate(p.player.dateOfBirth) : null,
4151
4160
  heightCm: p.player.heightInCm || null,
4152
4161
  weightKg: p.player.weightInKg || null,
4153
4162
  draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
@@ -4227,7 +4236,9 @@ export {
4227
4236
  normaliseVenueName,
4228
4237
  ok,
4229
4238
  parseAflApiDate,
4239
+ parseAflApiMatchTime,
4230
4240
  parseAflTablesDate,
4241
+ parseDate,
4231
4242
  parseFootyWireDate,
4232
4243
  toAestString,
4233
4244
  transformFryziggPlayerStats,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fitzroy",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "TypeScript port of the fitzRoy R package — programmatic access to AFL data including match results, player stats, fixtures, ladders, and more",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",