@tailgatefantasysports/seasonservice 0.5.1 → 0.6.1

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/SeasonService.js CHANGED
@@ -71,28 +71,64 @@ module.exports = class SeasonService {
71
71
  }
72
72
 
73
73
  /**
74
- * Get the previous complete season, including all types.
75
- * @summary Starting from `date`, walk backwards by one week to the nearest season.
76
- * Using that seasons `season` property continue to walk backwards to find all types of previous season.
74
+ * Get the last season that finished, including all of its types.
75
+ * @summary Walks backwards from `date` a whole season at a time until it reaches one whose
76
+ * every frame has ended, then collects that season's frames keyed by type.
77
+ *
78
+ * Stepping back one *frame* is not enough. Mid-season, one frame back is the same season's
79
+ * preseason. And in the gap between a season's preseason and its regular season, the
80
+ * nearest frame behind `date` is that same season's preseason — a season that has barely
81
+ * begun, not the last one to finish.
82
+ *
77
83
  * @param {object} date - date to begin search
78
- * @returns {object}
84
+ * @returns {object|null} frames keyed by type, or null when no earlier season exists
79
85
  */
80
86
  getPreviousFullSeason(date) {
81
- let frame = this.getPreviousSeason(date);
87
+ let frame = this.getSeason(date) || this._getSeasonDirection(date, -1);
88
+
89
+ while (frame && !this._isSeasonFinished(frame.season, date))
90
+ frame = this._skipSeason(frame);
91
+
82
92
  if (!frame)
83
- return null
93
+ return null;
84
94
 
85
95
  const full = { };
86
- let season = frame.season,
87
- newFrame = frame;
88
- while (newFrame && newFrame.season === season) {
89
- full[newFrame.type] = newFrame;
90
- newFrame = this.getPreviousSeason(new Date(newFrame.start - 1))
96
+ const season = frame.season;
97
+ while (frame && frame.season === season) {
98
+ full[frame.type] = frame;
99
+ frame = this._getSeasonDirection(new Date(frame.start - 1), -1);
91
100
  }
92
101
 
93
102
  return full;
94
103
  }
95
104
 
105
+ /**
106
+ * Whether every configured frame of a season has ended by `date`.
107
+ * @param {string|number} season - season key into the timeframes config
108
+ * @param {object} date - js date to measure against
109
+ * @returns {boolean}
110
+ */
111
+ _isSeasonFinished(season, date) {
112
+ const frames = this.timeframes[season];
113
+ if (!frames)
114
+ return false;
115
+
116
+ return Object.values(frames).every(({ end }) => end <= date);
117
+ }
118
+
119
+ /**
120
+ * Walks backwards past every frame of `frame`'s season to the nearest frame before it.
121
+ * @param {object} frame - season frame to step back from
122
+ * @returns {object|undefined} the preceding season's latest frame, if any
123
+ */
124
+ _skipSeason(frame) {
125
+ const season = frame.season;
126
+ while (frame && frame.season === season)
127
+ frame = this._getSeasonDirection(new Date(frame.start - 1), -1);
128
+
129
+ return frame;
130
+ }
131
+
96
132
  /**
97
133
  * Returns the regular season frame for a given season year, or null if not configured.
98
134
  * @param {string|number} seasonYear
@@ -127,21 +127,28 @@ describe('GetPreviousFullSeason', () => {
127
127
  end: new Date('2022-04-01')
128
128
  }
129
129
  }],
130
+ // Inside season 3, so season 3 is the *current* season and cannot be the previous one.
131
+ // The answer is season 2, whole. This previously returned season 3's preseason and
132
+ // regular frames — a partial, in-progress season reported as a finished one.
130
133
  ['Exists - start inside frame', new Date('2022-02-02'), {
131
- [types.preseason]: {
132
- season: "3",
133
- type: types.preseason,
134
- start: new Date('2022-01-01'),
135
- end: new Date('2022-02-01')
136
- },
137
134
  [types.regular]: {
138
- season: "3",
135
+ season: "2",
139
136
  type: types.regular,
140
- start: new Date('2022-02-01'),
141
- end: new Date('2022-03-01')
137
+ start: new Date('2021-03-01'),
138
+ end: new Date('2021-04-01')
142
139
  },
143
140
  }],
144
- ['Doesn\'t exist', new Date('2019-01-01'), null]
141
+ ['Doesn\'t exist', new Date('2019-01-01'), null],
142
+ // Mid-season, one frame back is the *same* season's preseason. Skipping only the
143
+ // current frame rather than the whole season would report season 3 as previous.
144
+ ['Exists - mid regular season', new Date('2022-02-20'), {
145
+ [types.regular]: {
146
+ season: "2",
147
+ type: types.regular,
148
+ start: new Date('2021-03-01'),
149
+ end: new Date('2021-04-01')
150
+ },
151
+ }]
145
152
  ])('%s', (_tName, inp, expected) => {
146
153
  // setup
147
154
  // execute
@@ -150,4 +157,39 @@ describe('GetPreviousFullSeason', () => {
150
157
  // assert
151
158
  expect(season).toStrictEqual(expected);
152
159
  });
160
+ });
161
+
162
+ describe('GetPreviousFullSeason — real calendar', () => {
163
+ // The configured singleton, not the bare class the synthetic fixtures above use.
164
+ const { SeasonService: configured } = require('../index');
165
+
166
+ // The case this method exists for: during the 2026 preseason the app asks what last season
167
+ // was, to show finished-season stats. It used to answer "2026 preseason" — the frame the
168
+ // date sits in — so the app showed preseason stats beside a regular-season fixture.
169
+ test.each([
170
+ ['from the 2026 preseason', new Date('2026-08-23T00:00:00Z')],
171
+ ['from mid 2026 regular season', new Date('2026-11-01T00:00:00Z')],
172
+ ['from the 2026 off-season', new Date('2026-05-01T00:00:00Z')],
173
+ // The gap between the 2026 preseason ending and week 1 kicking off. No frame contains
174
+ // these dates, and the nearest one behind them is the 2026 preseason.
175
+ ['from the day the 2026 preseason ends', new Date('2026-09-02T00:00:00Z')],
176
+ ['from the eve of the 2026 regular season', new Date('2026-09-08T18:00:00Z')]
177
+ ])('%s answers 2025, whole', (_tName, date) => {
178
+ const full = configured.getPreviousFullSeason(date);
179
+ expect(full).not.toBeNull();
180
+
181
+ const frames = Object.values(full);
182
+ expect(frames.every(f => f.season === '2025')).toBe(true);
183
+ expect(frames.map(f => f.type).sort()).toEqual(
184
+ [types.preseason, types.regular, types.postseason].sort());
185
+ });
186
+
187
+ test('never reports the season the date sits inside', () => {
188
+ const inPreseason = new Date('2026-08-23T00:00:00Z');
189
+ const current = configured.getSeason(inPreseason);
190
+ const full = configured.getPreviousFullSeason(inPreseason);
191
+
192
+ expect(current.season).toBe('2026');
193
+ expect(Object.values(full).some(f => f.season === current.season)).toBe(false);
194
+ });
153
195
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailgatefantasysports/seasonservice",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "Generic Season Service",
5
5
  "main": "index.js",
6
6
  "scripts": {