167996d99a
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
3.3 KiB
JavaScript
96 lines
3.3 KiB
JavaScript
// MLB park factors (Session 15) — pin the membership list + assert
|
|
// the expected magnitude of the headline parks. Park factors shift
|
|
// year-to-year but the directional signal (Coors hot, Oracle cold)
|
|
// is stable; the test catches obvious typos.
|
|
|
|
const pf = require('../../src/data/parkFactors');
|
|
|
|
describe('parkFactors', () => {
|
|
test('covers all 30 MLB teams', () => {
|
|
const codes = Object.keys(pf.PARK_FACTORS);
|
|
expect(codes.length).toBe(30);
|
|
});
|
|
|
|
test('every entry has hr/h/r as finite numbers', () => {
|
|
for (const [code, vals] of Object.entries(pf.PARK_FACTORS)) {
|
|
expect(typeof code).toBe('string');
|
|
expect(code).toMatch(/^[A-Z]{2,3}$/);
|
|
expect(Number.isFinite(vals.hr)).toBe(true);
|
|
expect(Number.isFinite(vals.h)).toBe(true);
|
|
expect(Number.isFinite(vals.r)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('Coors Field is the most extreme HR park', () => {
|
|
const hrs = Object.values(pf.PARK_FACTORS).map((p) => p.hr);
|
|
const maxHr = Math.max(...hrs);
|
|
expect(pf.PARK_FACTORS.COL.hr).toBe(maxHr);
|
|
expect(pf.PARK_FACTORS.COL.hr).toBeGreaterThan(120);
|
|
});
|
|
|
|
test('Oracle Park (SF) suppresses HRs heavily', () => {
|
|
expect(pf.PARK_FACTORS.SF.hr).toBeLessThan(95);
|
|
});
|
|
|
|
test('Coors also boosts hits and runs', () => {
|
|
expect(pf.PARK_FACTORS.COL.h).toBeGreaterThan(100);
|
|
expect(pf.PARK_FACTORS.COL.r).toBeGreaterThan(100);
|
|
});
|
|
|
|
test('most parks land within ±15% of neutral', () => {
|
|
// Coors + SF are deliberate outliers; check the rest cluster.
|
|
const codes = Object.keys(pf.PARK_FACTORS).filter((c) => c !== 'COL' && c !== 'SF');
|
|
for (const code of codes) {
|
|
const { hr, h, r } = pf.PARK_FACTORS[code];
|
|
expect(hr).toBeGreaterThanOrEqual(85);
|
|
expect(hr).toBeLessThanOrEqual(115);
|
|
expect(h).toBeGreaterThanOrEqual(90);
|
|
expect(h).toBeLessThanOrEqual(110);
|
|
expect(r).toBeGreaterThanOrEqual(90);
|
|
expect(r).toBeLessThanOrEqual(110);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getParkFactor', () => {
|
|
test('returns the row for known teams', () => {
|
|
expect(pf.getParkFactor('NYY').hr).toBe(pf.PARK_FACTORS.NYY.hr);
|
|
expect(pf.getParkFactor('COL').hr).toBeGreaterThan(120);
|
|
});
|
|
|
|
test('case-insensitive', () => {
|
|
expect(pf.getParkFactor('nyy').hr).toBeGreaterThan(0);
|
|
expect(pf.getParkFactor('Col')).toBe(pf.PARK_FACTORS.COL);
|
|
});
|
|
|
|
test('whitespace-tolerant', () => {
|
|
expect(pf.getParkFactor(' COL ')).toBeTruthy();
|
|
});
|
|
|
|
test('returns null for unknown / empty', () => {
|
|
expect(pf.getParkFactor('XYZ')).toBeNull();
|
|
expect(pf.getParkFactor('')).toBeNull();
|
|
expect(pf.getParkFactor(null)).toBeNull();
|
|
expect(pf.getParkFactor(undefined)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getParkFactorOrNeutral', () => {
|
|
test('returns the row when known', () => {
|
|
expect(pf.getParkFactorOrNeutral('NYY')).toBe(pf.PARK_FACTORS.NYY);
|
|
});
|
|
test('falls back to neutral 100s when unknown', () => {
|
|
expect(pf.getParkFactorOrNeutral('XYZ')).toEqual({ hr: 100, h: 100, r: 100 });
|
|
expect(pf.getParkFactorOrNeutral(null)).toEqual({ hr: 100, h: 100, r: 100 });
|
|
});
|
|
});
|
|
|
|
describe('immutability', () => {
|
|
test('PARK_FACTORS is frozen at the top level', () => {
|
|
expect(Object.isFrozen(pf.PARK_FACTORS)).toBe(true);
|
|
});
|
|
test('NEUTRAL is frozen', () => {
|
|
expect(Object.isFrozen(pf.NEUTRAL)).toBe(true);
|
|
});
|
|
});
|