Move domain models into domain package with schemas and validate with zod + elysia. Some maintenence as well

This commit is contained in:
Dominic Ferrando
2026-06-22 13:59:04 -04:00
parent ed5abac209
commit 6fa3d78571
16 changed files with 220 additions and 166 deletions
+30
View File
@@ -0,0 +1,30 @@
import type { Gender, Metrics, StandardUnit } from "@blade-and-brawn/domain";
import avgWeightDataJson from "./data/avg-weights.json" assert { type: "json" };
export const getAvgWeight = (gender: Gender, age: number) => {
type AvgWeightData = {
metadata: {
unit: StandardUnit;
};
weights: Metrics[];
};
const avgWeightData = avgWeightDataJson as AvgWeightData;
const avgWeights = avgWeightData.weights.filter((a) => a.gender === gender);
const firstAvgWeight = avgWeights[0];
if (!firstAvgWeight) throw new Error("No average weights");
const closest = {
diff: Math.abs(firstAvgWeight.age - age),
avg: firstAvgWeight,
};
for (const avg of avgWeights) {
const diff = Math.abs(avg.age - age);
if (diff < closest.diff) {
closest.diff = diff;
closest.avg = avg;
}
}
return closest.avg.weight;
};
+9 -16
View File
@@ -2,17 +2,18 @@ import {
Activity,
Attribute,
Gender,
getAvgWeight,
kgToLb,
lbToKg,
range,
type Metrics,
type ActivityPerformance,
type Player,
type StandardUnit,
} from "../src/util";
} from "@blade-and-brawn/domain";
import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
import defaultConfig from "./config.json" assert { type: "json" };
import defaultStandardsJson from "./data/default-standards.json" assert { type: "json" };
import { getAvgWeight } from "./avg-weights";
// SOURCES
// Squat, Bench, Dead Lift:
@@ -24,8 +25,6 @@ import defaultStandardsJson from "./data/default-standards.json" assert { type:
// 3 Cone drill:
// https://nflsavant.com/combine.php?utm_source=chatgpt.com
export * from "./util";
export type LevelCalculatorOutput = {
player: number;
attributes: Record<Attribute, number>;
@@ -33,12 +32,6 @@ export type LevelCalculatorOutput = {
export type Levels = Record<string, number>;
export interface Metrics {
age: number;
weight: number;
gender: Gender;
}
type NumberMetric = Extract<keyof Metrics, "age" | "weight">;
export interface Standard {
@@ -72,7 +65,7 @@ const metricPriority = (m: NumberMetric) => {
export const DEFAULT_STANDARDS_DATA = defaultStandardsJson as StandardsData;
export class LevelCalculator {
standards: Standards;
readonly standards: Standards;
public constructor(standards: Standards) {
this.standards = standards;
@@ -191,7 +184,7 @@ export class LevelCalculator {
const activityLevelsAvg = Math.round(
activityLevels.reduce((sum, curr) => sum + curr, 0) /
activityLevels.length,
activityLevels.length,
);
return activityLevelsAvg;
@@ -392,9 +385,9 @@ export class Standards {
// generate data
const allGenerators = this.cfg.activity[activity].enableGeneration
? this.standardsData[activity].metadata.generators.sort(
(a, b) =>
metricPriority(a.metric) - metricPriority(b.metric),
)
(a, b) =>
metricPriority(a.metric) - metricPriority(b.metric),
)
: [];
for (const generator of allGenerators) {
@@ -664,7 +657,7 @@ export class Standards {
weightUpper === weightLower
? 1
: (metrics.weight - weightLower) /
(weightUpper - weightLower);
(weightUpper - weightLower);
weightRatio = Math.max(0, Math.min(1, weightRatio));
// metrics.weight = lowerAgeWeightStandard.metrics.weight + (upperAgeWeightStandard.metrics.weight - lowerAgeWeightStandard.metrics.weight) * weightRatio;
-100
View File
@@ -1,100 +0,0 @@
import { type Metrics } from ".";
import avgWeightDataRaw from "./data/avg-weights.json";
// -------------------------------------------------------------------------------------------------
// DATA MODELS
// -------------------------------------------------------------------------------------------------
export type Player = {
name?: string;
metrics: Metrics;
};
export enum Attribute {
Strength = "Strength",
Power = "Power",
Endurance = "Endurance",
Agility = "Agility",
}
export enum Activity {
BackSquat = "BackSquat",
Deadlift = "Deadlift",
BenchPress = "BenchPress",
Run = "Run",
BroadJump = "BroadJump",
ConeDrill = "ConeDrill",
}
export interface ActivityPerformance {
activity: Activity;
performance: number;
}
export enum Gender {
Male = "Male",
Female = "Female",
}
// -------------------------------------------------------------------------------------------------
// Conversion utilities
// -------------------------------------------------------------------------------------------------
export type StandardUnit = "ms" | "cm" | "kg";
export const lbToKg = (lb: number | string): number => +lb * 0.453592;
export const kgToLb = (kg: number | string): number => +kg * 2.20462;
export const minToMs = (min: number) => min * 60000;
export const secToMs = (sec: number) => sec * 1000;
export const msToMin = (ms: number) => ms / 60000;
export const ftToCm = (ft: number) => ft * 30.48;
export const inToCm = (inches: number) => inches * 2.54;
export const cmToIn = (cm: number) => cm / 2.54;
export const msToTime = (ms: number, includeMs = false): string => {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const milliseconds = Math.floor(ms % 1000);
let formattedTime = `${minutes}:${seconds.toString().padStart(2, "0")}`;
if (includeMs) {
formattedTime += `.${milliseconds.toString().padStart(3, "0")}`;
}
return formattedTime;
};
export const range = (length: number) =>
Array.from({ length: length }, (_, i) => i);
export const clamp = (x: number, lo: number, hi: number) =>
Math.max(lo, Math.min(hi, x));
export const getAvgWeight = (gender: Gender, age: number) => {
type AvgWeightData = {
metadata: {
unit: StandardUnit;
};
weights: Metrics[];
};
let avgWeightData = avgWeightDataRaw as AvgWeightData;
const avgWeights = avgWeightData.weights.filter((a) => a.gender === gender);
const firstAvgWeight = avgWeights[0];
if (!firstAvgWeight) throw new Error("No average weights");
const closest = {
diff: Math.abs(firstAvgWeight.age - age),
avg: firstAvgWeight,
};
for (const avg of avgWeights) {
const diff = Math.abs(avg.age - age);
if (diff < closest.diff) {
closest.diff = diff;
closest.avg = avg;
}
}
return closest.avg.weight;
};