/** * Domain Value Object: StrengthOfField * * Represents the strength of field (SOF) rating for a race or league. * Enforces valid range and provides domain-specific operations. */ import type { ValueObject } from '@core/shared/domain/ValueObject'; import { RacingDomainValidationError } from '../errors/RacingDomainError'; export interface StrengthOfFieldProps { value: number; } export class StrengthOfField implements ValueObject { readonly value: number; private constructor(value: number) { this.value = value; } static create(value: number): StrengthOfField { if (!Number.isInteger(value)) { throw new RacingDomainValidationError('Strength of field must be an integer'); } // SOF represents iRating-like values (commonly ~0-10k), not a 0-100 percentage. if (value < 0 || value > 10_000) { throw new RacingDomainValidationError( 'Strength of field must be between 0 and 10000', ); } return new StrengthOfField(value); } /** * Get the strength category */ getCategory(): 'beginner' | 'intermediate' | 'advanced' | 'expert' { if (this.value < 1500) return 'beginner'; if (this.value < 2500) return 'intermediate'; if (this.value < 4000) return 'advanced'; return 'expert'; } /** * Check if this SOF is suitable for the given participant count */ isSuitableForParticipants(count: number): boolean { // Higher SOF should generally have more participants. const minExpected = Math.floor(this.value / 500); return count >= minExpected; } /** * Calculate difference from another SOF */ differenceFrom(other: StrengthOfField): number { return Math.abs(this.value - other.value); } get props(): StrengthOfFieldProps { return { value: this.value }; } toNumber(): number { return this.value; } equals(other: ValueObject): boolean { return this.value === other.props.value; } toString(): string { return this.value.toString(); } }